Per-crate changelog
Every publishable crate versions and publishes independently (ADR 0067); this page collects each crate's own CHANGELOG.md, newest release first per crate. Workspace-wide narrative — breaking bumps and coordinated releases — stays in the top-level changelog.
axiolid
0.5.1 - 2026-11-14
Added
Application::hidden_linesdraws perspective views: the portable hidden-line provider claims them (ADR 0107, #387). The perspective claim has its own conformance suite, run once per process and only by a call whose view is perspective; a failure refuses the call as the caseperspective claimof the hidden-line report. Visible faces still refuse a perspective view (Unsupported).
0.5.0 - 2026-11-13
Added
- Hidden-line drawings and visible faces join the
discrete(contract) andapplication(dispatch and exact scalar providers) bundles (#371, ADR 0105).Applicationgainshidden_lines,audit_hidden_linesandvisible_faces;ApplicationBuildergainswith_portable_hidden_linesandwith_portable_visible_faces, andApplication::portable()selects both. The descriptor advertisesMESH_HIDDEN_LINESandMESH_VISIBLE_FACES(Profile2d,ToleranceBounded). Each provider's conformance suite runs once per process, on the first call that reaches it; a failed verdict refuses the call. IntegrationDescriptor::enabled_featuresreportsmesh-hidden-lines,dispatch-mesh-hidden-linesandhidden-lines-provider.
Changed
Breaking:
ApplicationErrorSourceandProviderConformanceReportare#[non_exhaustive].ApplicationErrorSourcegainsHiddenLinesandProviderConformance;ProviderConformanceReportgainsMeshHiddenLinesandMeshVisibleFaces. Amatchon either needs a wildcard arm (#371, ADR 0105).Breaking: requires
axiolid-overlay0.4, soaxiolid::overlaycarriesOverlayErroras#[non_exhaustive]with the newHolesOverlapandPolygonsOverlap, andRegion::newrefuses rings that cross into each other. AmatchonOverlayErrorneeds a wildcard arm (#448, ADR 0104).
0.4.7 - 2026-11-03
Added
The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).mesh_hidden_linesre-exportsScalarVisibleFacesunder thehidden-lines-providerfeature (ADR 0094, #383).
0.4.6 - 2026-10-29
Added
mesh_sectionexportsCutRegion,CutRegionLimitsand the cut-region refusal names, soApplication::section_mesh_requestcan return the cut of a solid as closed regions (ADR 0092, #372).
0.4.5 - 2026-10-29
Added
Application::section_mesh_requestsections open meshes when the request hasSectionRole::Sheet(ADR 0091, #359).
0.4.4 - 2026-10-28
Added
Application::section_mesh_requestsections a closed oriented mesh and reports faces lying in the plane as regions with a side (ADR 0091, #359).
0.4.3 - 2026-10-28
Added
mesh-hidden-linesanddispatch-mesh-hidden-linesfeatures, with theaxiolid::mesh_hidden_linesmodule: the orthographic hidden-line drawing contract and its registry (ADR 0090, #138). Opt-in; the bundles do not include it.hidden-lines-providerfeature:axiolid::mesh_hidden_lines::ScalarHiddenLines, the exact scalar provider (axiolid-hidden-lines, #138).
0.4.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.4.1 - 2026-10-10
Added
- A
serdefeature (#267, ADR 0085) forwarding to theserdefeature of every representation package already in the build; withmodelit brings the geometry graph's versioned JSON and CBOR wire format (axiolid::model::wire). It enables no representation by itself.
0.4.0 - 2026-09-28
Changed
- Breaking: requires
axiolid-ray-mesh0.4, soaxiolid::ray_mesh(re-exported under the ray features) carries the newRayMeshError::TriangleIndexOutOfRange, andRayIndexqueries refuse an out-of-range candidate instead of skipping it. An exhaustivematchonRayMeshErrorneeds the new arm. - The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-arrangement
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-backend-cpu
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.2 - 2026-10-13
Fixed
- With
parallel, a one-threadCpuExecutionrunsinstallon a local pool of one worker (#259). It ran the closure directly, on whatever pool the caller was in, so a provider readingrayon::current_num_threads()there saw the caller's width instead of the configured one, and sized its work or its scratch for it. A context fromCpuExecutionBuilderstarts the pool when it is built and reports a failure there; one fromCpuExecution::portableorCpuExecution::detectstarts it on firstinstall. Clones share the pool.
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-backend-gpu
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-brep
0.3.7 - 2026-11-13
Added
ExactBRep::translated_exactly(#418): a translation that isNoneunless every vertex, frame origin, curve origin and control point moves with no rounding, so the copy is the exact translate and translating back returns the coordinates.
0.3.6 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.5 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.4 - 2026-10-03
Changed
ExactBRep::transformedrefuses a banked alignment curve (Curve3::Banked, #240) by name, as it does an elevated one: its cant is measured against+Z, which a motion would tilt.
0.3.3 - 2026-10-02
Added
ExactBRep::transformed: place an exact B-rep under a rigid motion (#223). Every vertex, curve and surface maps onto the same family -- planes, cylinders, elliptical cylinders, cones, spheres, tori, circles, ellipses, B-splines and the section curves that carry their surface -- with the topology, structural names and intervals kept; nothing is tessellated or refitted. A reflection keeps every frame right-handed (curved surfaces then read their angle backwards,u -> 2 pi - u, and their pcurves are reflected to match) and flips every face, so a solid stays outward oriented. Refused with the newTransformError: a non-finite transform, a linear part not orthonormal withinRIGID_TOLERANCE(a scale or shear), an elevated alignment curve, and under a reflection the pcurve and carrier families with no closed-form reflection here (quadric and torus section graphs, traced, lifted and intrinsic curves on curved faces).
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-27
Added
ExactBRepBuilder::append: copy another exact B-rep's vertices, edges, loops, faces and shells, with their curves, surfaces, intervals and names, and return the new shell handles; optionally with every face used reversed, which turns an outer shell into a void (#111).
axiolid-brep-audit
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-27
Changed
- An implicit pcurve (ADR 0077) is parameterised by its cells, not in proportion to its edge. It passes when every lifted sample projects onto the edge within tolerance, inside the edge's span, in the order the use runs, and starting and ending at the use's ends. Every other pcurve is still checked against the edge at proportional parameters.
axiolid-brep-boolean
0.1.15 - 2026-11-13
Added
BooleanError::VertexOffCurve(#445): a result is checked before it is returned, and refused by name when a vertex lies off the curve of an edge it bounds, or off an edge use's pcurve lifted at its end, by more thanmin(eps, rounding floor)for an exact report, the floor alone atTolerance::ZERO, andepsotherwise. The check runs inside the cut, so the finer, extending and rounding-end retries apply to it, and again after the result is moved back from a recentred cut.
Fixed
- A difference or union far from the origin returned a vertex off its curve with an exact report (#445): a window's crossing 1.2e-6 past a face replaced a feature cut within the parameter-relative dedup slack of
pieces(5.6e-6 at a parameter of 5.6e6), so the column less a corbel at(6e5, 5.6e6, 200)had a vertex 1.2e-6 off its edge, the union was refused as not sewing atTolerance::METRE, and at 1e-7 andTolerance::ZEROthe union's volume was 1.8e-8 off. A window end never replaces a feature cut now; the corners are the corbel's own vertices bit for bit and every volume is exact. - A loop reaching a pole or apex along a seam was read as winding and given a second seam, so the boolean was refused with
TangentSplitorAssembly; a partial revolution's apex included (#379). A step over a pole or apex is no longer counted as a turn. - Unions far from the origin sew their kept faces and classify points near a face boundary (#418, #419): operands whose common vertex box lies four or more diagonals from the origin on an axis are recentred on that axis exactly before the cut and the result moved back (
ExactBRep::translated_exactlyproves every coordinate moves with no rounding, else the operands are cut where they are; so are operands whose recentred cut is refused, as planar faces charted in world coordinates, #227, keep parameters as far out as before). The #356 column and corbel at(6e5, 5.6e6, 200)was refused in 132 of 200 turns (117 "do not sew", 15 "too close to a face boundary to classify"); every turn is now one solid. The report refers to the operands as given; operands nearer the origin are cut bit for bit as before. The next release needsaxiolid-brepwithtranslated_exactly.
0.1.14 - 2026-11-07
Fixed
BooleanReport::rounding_flooradds the move of an angular residue at the rounding floor (2^-40rad, read without a decision) over the operands' extent,2 sqrt(3)times it (#421): a consumer widening by the floor now covers the unrecorded angular residues too. The floor is that widening, not the threshold for a decision: a residue between2^-40times the extent and the floor may be a reported decision, so a check oflinear() <= rounding_floor()is about 4.5 times looser than before.- A floor-standing door
3e-7to9e-7short of a wall's face under a millimetre tolerance is cut, not refused as "the kept faces did not sew into a valid exact B-rep" (#409). The door's end face is read as the wall's, but its floor edge stops short of the wall's floor edge by more than the splitter's fixed relative slack (1e-7), so the wall's floor did not split round the door's footprint. A boolean refused at the caller's tolerance and at the finer one is now cut a last time at the caller's tolerance with such a line edge extended to the boundary it falls short of, as the door past the face already meets it; the move is reported (MergedPoints, at least the gap). The extension is tested for a difference at3e-7,7e-7and9e-7short and3e-7and7e-7past a straight wall, for a union and an intersection at3e-7either side, and for a curved wall at9e-7. A door1e-6or more short is past the finer tolerance (9.8e-7) and is cut exactly at it, its skin kept.
0.1.13 - 2026-11-03
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
Fixed
- A box less a torus whose tube leaves through a face the ring's radius from the axis was refused as
Undecided(#381). The traced section touches the latitude circle where the tube's quarter faces meet; its double root split into cuts2e-8apart, which were never merged for a traced section. They are now one cut within tolerance, reported asMergedPoints, so the box less the torus cuts at a micrometre;Tolerance::ZEROstill refuses. - A column united with a revolved bend whose start cap lies in the column's foot did not sew (#382). The plane through the torus axis meets the torus in the cap's circles as a traced section while the cap imprints them in closed form, and the two copies of one stretch were kept apart. A traced piece along an operand's line or conic edge now takes that edge's curve, so the union is exact at
Tolerance::ZEROand, placed by a general rigid motion, cuts at a micrometre.
0.1.12 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.1.11 - 2026-10-18
Added
FINER_FACTORandBooleanReport::finer(#332): a boolean refused at the caller's tolerance is cut again at2^-10times it, and its report says so.BooleanReport::mergedkeeps the flag.
Fixed
- Curved walls with abutting openings are cut (#332). Where one opening's radial side reaches a few micrometres, or just under the tolerance, past its neighbour's, the two side planes are turned by more than the angular tolerance and lie within the linear one in the wall: read within the tolerance, a region on one of them was "a point too close to a face boundary to classify", and the sections' cuts merged at a point on neither face's boundary, so the faces did not sew. Such a boolean is now cut again at the finer tolerance, where the sides are apart and decided by the exact predicates. A split face's piece ends now weld into one vertex only where they also lie within the tolerance on the surface: the relative slack in parameters spans
7 umof an arc of radius24, and welded two vertices a split had kept apart ("two pieces leave a vertex of a split face in one direction"). A wall of radii6to6.25less five wedge openings, abutting by15 umand0.6 mm, and one of radii24to24.1whose openings abut by7 um, are cut at the closed-form volume (within1e-9), every reading within the finer tolerance. A boolean that succeeds at the caller's tolerance is unchanged; one refused at both returns the first refusal. A web hole half a tolerance off a filleted flange, refused as anUnsupportedContactbefore, is now cut at the finer tolerance at the reference volume. - A box whose face touches a curved wall's inner cylinder along a ruling is cut at the caller's tolerance (#327). Every point of the box's face near the ruling lay within the tolerance of the cylinder and was not classified ("a point lies too close to a face boundary to classify"). Where no point of a region decides with the ray's start read within tolerance, its points are classified again reading the start exactly: on a support only within the rounding floor, but for the faces read as the point's own support. The quarter ring of radii
4.8to5less a box on the tangent plane, or a unit in the last place either side of it, is cut at the closed-form volume (within1e-12), its report within1e-14.
0.1.10 - 2026-10-16
Fixed
- A face of one operand on a face of the other is read facing the right way when the other face's loops wind clockwise (#324). A quarter-ring wall less a niche whose inner arc lies on the wall's inner cylinder, or one unit in the last place inside it, was refused as "the kept faces did not sew into a valid exact B-rep": the two cylinders were read as one surface and split along each other's boundaries, but the niche's inner face (an extrusion's face on its profile's inner arc, whose loops wind clockwise) was read with its outward normal turned round, so the wall's patch on it seemed to face the other way and the difference kept it. A region on the other operand's face now reads that face's outward normal with its winding, as a region of its own face already did. Both niches are cut to the closed-form volume, the coincidence reported (
CoincidentSupports, at most4e-15).
0.1.9 - 2026-10-10
Added
- Booleans cut tilted elliptical-cylinder walls, the round walls an oblique exact extrusion sweeps from arcs and circles (#287), upward or downward. A slab less an oblique round shaft or rounded opening was refused as "a curve or surface could not be evaluated"; it is now exact (an empty report, also at
Tolerance::ZERO) with the volume area x depth. A plane's cut, an ellipse or circle, gets the closed-formSinusoid2pcurve in the wall's angle, and a ruling a vertical line. Whether a plane is parallel to such a wall's axis or touches it is decided by exact predicates only, never read within tolerance.
0.1.8 - 2026-10-09
Fixed
- A floor-standing door
1e-6to5e-4past a wall's face under a millimetre tolerance is cut again, not refused as "the kept faces did not sew into a valid exact B-rep" (#291). Merging a cut on an imprinted edge into the edge's own end within the tolerance (#276) left those faces unsewn; a boolean refused after such a merge above the rounding floor is now cut again with ends merged only within the rounding floor, as before #276, and its report holds only the second run's decisions. The merge within the tolerance stays: the door a rounding error (1e-12-1e-9) past the face still needs it.
0.1.7 - 2026-10-09
Fixed
- A box is cut by a reflected cylinder wall -- a prism extruded along
-z, whichextrude_profile_exactbuilds by reflecting the+zone (#275) -- as by the same wall built directly, not refused as "the kept faces did not sew into a valid exact B-rep" (#288). The reflected wall reads its angle2 pi - u, a turn away from the surface inverse's(-pi, pi]. Where a section ended on such a face's edge, the cut was placed on the edge's line pcurve a turn of the pcurve's own parameter from the cut point -- a turn is the same point only on a conic's or a sinusoid's pcurve -- so the face was not split. A candidate parameter must now land on the cut point. Reflected spheres, cones and tori are cut by boxes under every operator too.
0.1.6 - 2026-10-09
Fixed
- A door standing on a wall's floor face and reaching a rounding error past the wall's face (or stopping that short of it) is cut, not refused as "a point too close to a face boundary to classify" (#276). Where two coincident faces imprint each other's edges, a cut within the tolerance of the imprinted edge's own end is that end (
MergedPointsabove the rounding floor), and cuts within the tolerance of each other merge as on a section; the piece between was a fraction of the tolerance long and lay on the other face's boundary.
0.1.5 - 2026-10-03
Added
BooleanError::ToleranceExceeded(#251): a decision within tolerance was asked for beyond the caller's tolerance, any at all atTolerance::ZERO. The report's session refuses it by name (and asserts in debug builds) rather than return a result its report misdescribes; no reading asks for one.BooleanError::UnsupportedContact(#243): a plane read as touching a cylinder within tolerance is crossed by a curve whose meeting with their contact ruling this stage cannot place (a curve of the plane that is no line, a curve of the cylinder that is no ruling or conic). Refused by name rather than sewn against exact roots that disagree with the reading.
Fixed
At
Tolerance::ZEROthe report is empty again (#251, the #236 contract). A support reading the exact predicate rejects was taken when itsf64measure was exactly0, and reported as a decision withlinear0: a round hole across a wall under a general rotation, whose axis and the wall's normals round to the samef64vector though the numbers given (a frame orthonormal only to rounding) are not perpendicular, was readPlanePerpendicularToAxisat zero. A tolerance with a zero part now never takes a reading the exact predicate rejects (coincident supports; a plane parallel or perpendicular to a cylinder's axis or touching it): the exact answer stands, and the general closed form cuts the hole, exactly, with an empty report. Point readings already recorded only residues above the rounding floor and within the tolerance, never at zero. Both positive parts: unchanged.A round web hole touching an I-beam's flange is no longer refused when the beam has root fillets (#249). The hole is then tangent to each fillet cylinder where the fillet meets the flange, and the hole/fillet section (perpendicular cylinders, axes skew by the difference of their radii) is a quartic with a double point there: two loops round the fillet crossing at the fillet/flange edge. It was refused ("a section curve the general boolean does not build"): the trace over the fillet face's parameter box met the crossing on the box's edge. Now (new
tangencymodule):- a trace that cannot be decided in a face's box of a periodic carrier is taken over the carrier's whole turn, where the crossing is inside and both loops end at it (ADR 0077); a loop closing through such a vertex is cut there like at any other cut;
- a section on a face is cut at a smooth edge of the face (a fillet running tangent into the flange or web) where the edge itself crosses the section's other surface, not where the section touches the adjacent surface (a double root that rounding splits or loses);
- a traced section on the face's own surface keeps the traced curve as its pcurve, instead of a second trace over the face's box;
- with the flange read as touching the hole within tolerance (#243), the double point is placed on the contact, and a traced section on a cylinder in a contact is cut where it crosses the contact ruling. A hole that is exactly tangent (exact axes, dyadic sizes) gives an exact result with an empty report at
Tolerance::ZERO; under a general placement the report carries thePlaneTouchesCylinderreading. A hole a fraction of the tolerance into or short of the flange, which meets each fillet in two arcs the contact would have to join (no single move of one operand makes both readings hold), is refused by name (BooleanError::UnsupportedContact). Holes cutting the fillets transversally, or ten tolerances into the flange, are decided exactly.
A plane touching a cylinder within tolerance is read the same way by every face pair (#243). A round hole tangent to a planar face (an I-beam's web hole touching the flange) placed under a general rotation, or reaching a fraction of the tolerance into or short of the face (the #234 roof plane in a column), was refused ("split face pieces do not close", "point too close to a face boundary", "do not sew"): the plane/cylinder pair read one contact ruling while the cap disks' chords, the circles of faces across the axis and the plane's own edges kept the exact roots,
2 sqrt(2 r d)apart. The reading is now taken once per pair of supports, over the largest common box of their faces, and every crossing of a curve on one of the two by the other is placed on the contact ruling, as the moved plane gives it (TangentCrossing); points where the given and the moved plane disagree on a side never decide a piece or region. The result is the exact boolean of operands whose plane moved by at most the reportedPlaneTouchesCylinderdistance. Exactly tangent pairs are still decided by the exact predicates, and succeed atTolerance::ZEROwith an empty report.Cuts merged within tolerance on a circle or ellipse on both sides of its parameter origin (one point named at
0and at2 pi) averaged to the opposite side of the curve, so a flush hole's circle was split in the wrong place and the result did not sew (#243). They are unwrapped to one turn first.A section's window reaches
2^-20of its diagonal past the two faces' common box (#243). Its planes ran through the faces' extremes, so a section touching a face there (a tool resting on a face, atTolerance::ZERO) left the window next to the touching point, and the piece between could not be classified.
0.1.4 - 2026-10-03
Added
BooleanReport::rounding_floor(#244): the absolute rounding floor that applied to a result, in the operands' length unit. Constructed points closer than it count as one point and are not reported, so a consumer widening distances on a result, exact or not, widens them by it.BooleanReport::extentis the extent it was scaled from, with its definition (the largestmax(|x|, |y|, |z|)over the operands' vertices and their analytic surfaces' origins widened by their radii), and the publicROUNDING_FACTOR(2^-40) is the relative factor.mergedkeeps the larger floor.
0.1.3 - 2026-10-03
Added
boolean_with_reportreturns, with the result, aBooleanReportof the within-tolerance decisions that fired (#236): oneToleranceDecisionperToleranceDecisionKind(coincident supports, contact, a point on an edge, a section along an edge, a tangent crossing, merged points, an iso-curve, a plane parallel or perpendicular to or touching a cylinder), with the furthest it moved (linear) or turned (angular) the operands. An empty report means the result is the exact boolean of the operands as given.booleanis unchanged.
Fixed
- Placed differences succeed at
Tolerance::ZERO(#236). Naming a point on the curve or surface it was evaluated from compared itsf64round-trip residue with the linear tolerance, so at zero every boolean failed withBooleanError::Evaluation; that bookkeeping, and welding two evaluations of one vertex, now allow the rounding of the operands' extent (2^-40of it). - Readings about the operands' own surfaces (coincident supports, a plane parallel or perpendicular to or touching a cylinder) ask an exact dyadic predicate first: faces exactly coplanar, parallel or perpendicular, as operands placed by matrices with entries
0and+-1meet, are decided exactly and read nothing within tolerance.
0.1.2 - 2026-10-03
Added
BooleanError::NearCoincidence: features chained within tolerance of each other over more than the tolerance, which no single perturbation within tolerance reconciles, are refused by name (#228).- The crate docs state what a result guarantees: which decisions are exact, which are taken within the caller's tolerance, and that the result is then the exact boolean of operands moved by at most it.
Fixed
- Operands placed by independent rigid motions cut each other (#228). Their coincident, parallel or tangent faces agree only up to rounding, which the exact predicates saw: an opening flush with a wall's faces was undecided, an arched opening's tangent jambs left slivers or no cut, and a plane
1e-17off parallel or perpendicular to a cylinder's axis gave an ellipse nothing could evaluate. Within tolerance, a section now runs along an edge it lies on, a tangent crossing is cut once where it meets the edge, cuts and boundary splits closer than tolerance are one point, and such a plane cuts the cylinder in rulings or a circle.
Changed
- Every within-tolerance decision uses the caller's
Toleranceonly: distances its linear part, with no built-in1e-9floor, and the coincidence and contact tests on normals and axes its angular part instead of a fixed1e-9. WithTolerance::METREresults are unchanged. - Work between faces that cannot meet is skipped (#228). Each face and edge gets a sound box from its surface or curve in closed form, enlarged by the linear tolerance; face pairs whose boxes are apart are not sectioned, a section line or conic is cut only against edges whose boxes meet both faces' common box (and at that box's boundary), classification rays skip faces whose boxes they miss, and split faces ignore cut points outside their boxes. A wall losing ten placed windows one at a time went from 1.67 s to 0.28 s in release (
cargo bench -p axiolid-benchmark --bench exact_openings).
0.1.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.1.0 - 2026-09-27
Added
Surfaces tangent along a whole curve and crossing there: the line of contact is a section edge (the tangent-contact rule now applies to closed forms only), and pieces leaving a vertex with the same direction and bend are ordered by their chords a small way out.
A traced pair that only touches adds no section, as the closed forms' touching does. A pcurve whose trace on its face cannot be decided falls back to the section's own space curve read on the face, and failed traces are not repeated. Each surface pair's closed form is computed once per boolean.
Two B-spline faces meeting each other: their section is traced in both faces' parameter boxes and carried on both (
Curve3::PairSection), its pcurve on each read from the solve. The last refusal by face type is gone; sections whose branches cross where the surfaces touch stay refused by name.Faces that wind round their surface without a seam edge (a dome bounded by its rim alone, a can by its two rims), as files may deliver them. The boolean first gives each a seam edge along the iso-curve where its loops wrap, joining the lower loop, the seam, the upper loop or a pole, and the seam back into one loop.
Sections through a sphere's pole or a cone's apex are cut there, and the collapsed pole piece is split where they end. Meridians and latitudes, cone rulings and circles, and a torus's tube and ring circles get their straight pcurves, affine in the curve's own parameter.
B-spline faces (#167, ADR 0075 stage 3) against planes, quadrics and tori:
- The section is traced on the spline (ADR 0077).
- On the analytic face its pcurve is the same space curve read in the face's parameters (
Curve2::Lifted), so it shares the edge's parameter. - An edge next to a B-spline face is cut where it meets the section's other surface.
- Classification rays meet B-spline faces through the spline trace.
- Two B-spline faces meeting each other are refused by name (
UnsupportedSection).
Faces on spheres, cones, tori and elliptical cylinders (#167, ADR 0075 stage 2), meeting in any section #119 builds:
- A section with no line or conic is traced inside one face's parameter box (ADR 0077).
- Every section on every analytic face gets an exact implicit pcurve, cut out of the other surface's traced equation between the section's ends.
- A sphere's pole or a cone's apex closes loops as a collapsed piece that is no edge.
- Seam circles are cut by the cone of normals along them.
- Section branches that cross (a Steinmetz pair) are split where they meet.
- Frame components that are only rounding residue are cleared before intersecting.
Operands that touch (#167, ADR 0075 stage 2): faces on one surface share their overlap (each face's edges are imprinted on the other, and a region on the other solid's boundary is kept once by normal agreement); sections along an existing edge split only the other face; tangent contact adds no section; pieces leaving a vertex in one direction are ordered by curvature; solids meeting along an edge are paired radially around it so each stays manifold. Cavities go to the smallest solid around them, in results of several solids too.
section_edges(#167, ADR 0075 stage 1): the exact intersection curves of two exact B-reps' faces, each trimmed to where it lies inside both faces. Crossings with a boundary edge are found against the adjacent face's surface, or across a seam against the plane through the ruling.boolean(a, b, operator, tolerance)(#167, ADR 0075 stage 1): the exact union, intersection and difference of two exact solids whose faces lie on planes and cylinders and meet in lines, circles and ellipses -- not only vertical columns. Regions are classified by exact ray parity with certified face membership and sewn into shells; cavities become voids. Every result audits clean and measures exactly.split_face(#167): a plane or cylinder face cut along its section edges into regions, traced in the face's parameters with exact pcurves (lines, conics, rulings, circles about the axis,Sinusoid2for oblique cuts).
axiolid-capi
0.4.0 - 2026-11-13
Changed
- Breaking for Rust callers only. The public C-mirroring enums
AxiolidStatus,AxiolidOperation,AxiolidRepresentation,AxiolidExactnessandAxiolidGeometryKindgain variants (MeshHiddenLines,MeshVisibleFaces,HiddenLines) and are now#[non_exhaustive], so later C-ABI-additive variants are no longer Rust breaks. The C ABI is unchanged and additive (abi_patch1).
Added
- Visible faces through the C ABI (#386):
axiolid_v0_4_visible_faceswithAxiolidVisibleFaceLimits; the result is read withaxiolid_v0_4_visible_faces_counts,_copy(regions, rings, points and triangle indices as flat offset-indexed tables, nothing written unless every capacity covers its count) and_evidence.AxiolidFaceSourcedistinguishes group regions from near-plane caps,AxiolidGeometryKindgainsVisibleFaces, and the capability table listsMeshVisibleFaces. Arguments, refusals and mesh ids behave as for hidden lines. Existing symbols and layouts are unchanged. - Hidden lines through the C ABI (#473):
axiolid_v0_4_hidden_lineswithAxiolidView,AxiolidViewMesh(roles, group modes including planar),AxiolidEdgeSelection(including group boundaries) andAxiolidHiddenLineLimits; the result is read withaxiolid_v0_4_hidden_lines_count,_copyand_evidence.axiolid_v0_4_context_last_error_meshreturns the id of the input mesh a refusal names.AxiolidOperationgainsMeshHiddenLinesandMeshVisibleFaces,AxiolidGeometryKindgainsHiddenLines, and the capability table listsMeshHiddenLines. All arguments are validated at the boundary.abi_patchis 1; existing symbols and layouts are unchanged.
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-collide
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-constraint2d
0.1.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.1.2 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.1.1 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.1.0 - 2026-10-02
Added
tangent_circles(#159, ledger row B14): every circle tangent to three given points, lines or circles, in any combination (Apollonius' problem and its degenerate cases), returning every real solution. Each of the three constraints may independently require internal or external tangency for a circle constraint, or either side for a line constraint, enumerated as sign choices; solutions are deduplicated and sorted for a deterministic order. Refused by name: non-finite input, a non-positive circle radius, a zero line direction, and a configuration whose elimination system is singular for every sign choice (three coincident or otherwise inseparable constraints).
axiolid-construct
0.4.0 - 2026-11-13
Changed
- Breaking: requires
axiolid-overlay0.4, whoseOverlayErroris#[non_exhaustive]with the newHolesOverlapandPolygonsOverlap, and whoseRegion::newrefuses rings that cross into each other.ProfileFinding::DegenerateRingcarries thatOverlayError, so amatchon it needs a wildcard arm (#448, ADR 0104). revolve_arc_ringrefuses an axis origin withz != 0as "exact revolution about an axis out of the profile plane" (#450); it used to accept it unless the section touched the axis.- The exact revolution is built about the axis as given (#450). An axis exactly along y (either sense) builds in place with the same geometry as before (the pcurves of a wall's loop are added in another order); any other axis lying exactly in the profile plane (
zzero in its direction and origin) maps the section rigidly into the axis's own frame, revolves it about +y there and places the solid back withExactBRep::transformed, composites, holes and partial turns included. A tilt of a milliradian was silently snapped onto y within the tolerance, so a groove about an axis tilted 1e-3 rad at radius 2 sat 2 mm from where it was asked for (the mesh/exact comparison read a certified 4.5e-3 against a 1e-3 budget); it is now exact, and a section the tilted axis puts on both sides is refused as crossing it. An axis with a component out of the plane (a directionzor an originzother than exactly zero) isUnsupportedInput("exact revolution about an axis out of the profile plane"): its edges sweep hyperboloids, so there is no tolerance band to offer; a zero or non-finite axis is invalid input.revolve_arc_ringrefuses an origin off the plane the same way. In the axis's frame a vertex exactly on the axis is put on it and a disc edge exactly perpendicular to it keeps equal heights; a vertex the rotation's rounding cannot keep on its side, and an edge not exactly perpendicular whose heights round equal, are refused by name. - An arc of a lowered contour evaluates
sinandcosexactly at a quarter turn (#379): a parameter within2 * EPSILON * max(1, |t|)of one (every form a producer writes,FRAC_PI_2,-PI / 2,1.5 * PI,PI + FRAC_PI_2, ...) puts the end of the arc at exactly0and+-1in that coordinate, where it used to be up to0.55 * t * EPSILONoff (0.18at the quadrants of a turn or two). A ring that lowered before is unchanged except at the arc's ends and at split vertices that land on a quadrant, by less than the error of the point itself; a parameter 1e-12 from a quarter turn keeps its angle. The joint check reads the arc's radius timesmax(1, |t|)in its scale, so an arc of a large parameter closed by a line between its raw ends still lowers atTolerance::ZERO. This makes an arc end on a revolution axis through its centre exactly on it, which the exact revolution needs. A rounding gate like the joint rule (#250), not reported.
Added
revolve_profile_exactandrevolve_arc_ringrevolve a full turn of a profile that touches the axis (#379): along edges lying on it, at straight edges ending on it exactly perpendicularly (a disc cap: a solid cylinder, a stepped shaft), at any other straight edge ending on it (a cone up to its apex: a cone, a bolt, a spinning top, a top with a cavity) and at an arc ending on it with its circle centred on it (a sphere up to its pole: a ball, a domed column, a hemisphere cap). An axis edge sweeps no face. A vertex on the axis is admitted when one of its two edges lies on the axis; the contact is classified with the exactside_of_axissign, no tolerance. An apex or pole is a vertex of the B-rep, not a degenerate edge: the cone is framed at its apex with radius zero, the sphere on the axis at the height equidistant from both arc ends, and the wall's loop leaves a gap along the pole line in its parameters, which the boolean and the deviation oracle close. Whether an arc is a sphere's meridian is decided within64 * EPSILONof the largest magnitude it is computed from: its coordinates, the axis's and, about an axis in another direction, the given section's (a bulge cannot encode a quarter arc exactly, and a radius is a difference of numbers of the axis's size); the sphere is placed exactly on the axis, and the gate is not reported, as it only chooses between that sphere and a refusal. A section crossing the axis, touching it at a vertex with no edge along it (a pinch point), ending on it at an arc whose circle is not centred on it (a lemon, an apple, an ogive nose: a spindle or horn torus), at an arc with both ends on it or at an arc running across it staysUnsupportedInputby name, as does a vertex within the tolerance of the axis but not on it to the bit and an edge ending on the axis within the tolerance of perpendicular to it but not exactly, and a straight edge so near perpendicular that a cone's angle cannot carry its slope (below). A semicircle (a bulge of exactly one) between two vertices on the axis is split at its midpoint, as the lowering splits a half turn, and revolves to a ball; another bulge between them is a lemon and is refused. A section with several contacts (a cavity along the axis) gives one solid per outermost piece with the nested pieces as void shells, nested by the heights of every face's stations, poles included; a solid inside a cavity is refused.
Fixed
- A cone whose edge is nearly perpendicular to the axis was built with an angle that does not carry its slope when the tolerance did not cover it (#379): at
Tolerance::ZERO, a frustum[(1,0),(2,1e-12),(2,1),(1,1)]came backOkwith its pcurves 7e-5 off their curves, and the same for a cone's apex edge. The exact revolution now refuses an edge whoserise * tan(angle)differs from its radius change by more than the tolerance or eight machine epsilons of its largest coordinate, whichever is larger, as an edge so near perpendicular to the axis that a cone cannot carry its slope. revolve_arc_ringmirrored a section on the negative side of the axis without walking it backwards, so its walls faced inward and its signed volume was negative; the mirrored section is now walked the other way round (#379).
0.3.29 - 2026-11-03
Added
ExactProfileValidator::validate_withandvalidate_contours_withtakeArcCheckOptions; with a clearance, segments of the lowered contour whose distance has a local minimum below it away from their meeting points, or that form an exact cusp, are aProfileFinding::Clearancenaming both segments (a narrow wedge at a nonzero angle is not reported), with a certified lower bound on their distance and anundecidedflag for a pair the exact check could not clear.ValidateProfile::validateand the extruder are unchanged (#388, ADR 0095).profile::paired_profile_ringsandPairedRings(#361): profiles whose rings pair segment for segment (lines, circular and elliptic arcs, matched by ring and segment index after orientation, a derived profile's transform applied after the snap) are flattened together, each segment into one common number of equal parameter steps, with the deviation of every section interpolated between them. A polyline or B-spline segment, another family or a different segment structure returnsNone.
Changed
contour_to_arc_ringand the profile validator lowerCurve2::Polylinecontour segments exactly: one straight ring vertex per polyline vertex the segment passes, stored points taken bit for bit, a domain outside[0, segments]refused as degenerate rather than clamped. A contour now needs two ring vertices, not two segments, so one closed polyline or one full circle is a whole contour; theTooFewSegmentsmessage says so (#389).
Fixed
extrude_profile_exactran no profile check and built a solid from a contour with a self-contact (a chord crossing an arc, a bow tie, a pinch), a hole outside the outer contour, or holes that cross each other or the outer contour, also as the member of a composite. It now runs the exact arc polygon check on each contour (and on each composite member) and refuses such a profile asInvalidInputnaming the defects; a nesting the exact check cannot decide is refused asUnsupportedInput. Valid profiles, including those with an accepted isolated contact or an edge or arc below the tolerance, extrude unchanged, and the lowering's own refusals keep their error class (#401).The mesh revolution (and the tapered one) treats a profile vertex within
tolerance.linear()of an in-plane axis as on it: it is projected onto the axis and welded into one axis vertex, and the projection distance is taken from the chord budget, so the reported bound stays within the budget. A crescent whose two arcs meet on the axis at x of about 6e-17, or a vertex 1e-12 off a tilted axis, was classified strictly off it: a full turn meshed a pinch with a tiny orbit instead of refusing it (fail-open), and a partial turn left the vertex unwelded. A full turn now refuses the pinch by name (#367). Behaviour that changes with it:- A vertex on the far side of the axis within the tolerance is welded rather than a crossing:
[(-0.5t, -1), (1, 0), (0, 1)]over a half turn wasInvalidInputand is now meshed. - A vertex close to the edge of the band can give
BudgetExceeded, as the projection leaves little budget for the turn; a distance within rounding of the tolerance is refused by name. - A ring that the projection flattens onto the axis (a hole whose vertices all lie in the band, or an outer ring that does) is refused by name; it would have vanished from the solid or left it empty.
- A tapered revolution with a vertex inside the band at one end and outside it at the other is refused as touching at one end only.
- A tapered vertex on the axis at both ends is welded when its positions along the axis agree within the tolerance (it was compared bitwise, which a tilted axis almost never satisfied); the difference is charged to the budget.
- A vertex on the far side of the axis within the tolerance is welded rather than a crossing:
The caps of
extrude::extrude_profileand of a loft avoid sliver triangles (doubled area within the tolerance squared) whenever a triangulation avoids them, and refuse by name a cap triangle that is both a sliver and flat to rounding. A polyline profile with runs of collinear vertices left cap triangles a few ulps off a line, which placement rounds to zero area andaudit_meshreads as an open mesh (#415). A sliver that is not flat (a fine chord's) is still meshed, as it is legitimate there.triangulateandtriangulate_withare unchanged.
0.3.28 - 2026-11-03
Added
profile_validate::ExactProfileValidatorimplementsValidateProfileforContourProfileandProfile. It decides closure, self-contact and hole nesting exactly on the contour as the exact path lowers it, or refuses with a typed finding naming the segments (#352, ADR 0095).
0.3.27 - 2026-11-03
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
Fixed
- The mesh tapered revolution meshes a profile whose start and end rings touch an in-plane axis (a cone, a double cone): a ring vertex on the axis at the same place at both ends is welded across the stations as in a plain revolution, and the closed output holds the swept volume (#364). A vertex on the axis at one end only, or one that slides along the axis between the ends, is still
UnsupportedInput, now named as such; welding a sliding vertex would have swept the wrong solid. A partial tapered turn closes an isolated touch into a sector point and welds it asrevolvedoes; it was refused as a pinch. - The mesh revolution judges an axis leaning out of the profile plane (#362). The sweep is free of self-intersection exactly when the profile lies on one closed side of the axis's projection into the plane, decided by the exact side test of an in-plane axis; rings on both sides are
InvalidInput, which covers an axis piercing the profile. A profile that meets the point where the axis crosses the plane, or is not certified clear of it by more than the tolerance (measured along the projection, not as an orbit radius), isUnsupportedInput; an axis perpendicular to the plane isInvalidInput. The tapered revolution judges the whole blend: a vertex on the projection at both ends is judged over its closed span of offsets, and a vertex crossing the projection between the ends is invalid input. Such axes were accepted unchecked. - The mesh profile flattening refuses a contour whose consecutive segments, or last and first, leave a gap beyond the linear tolerance with
InvalidInput, as the exact path does; it used to bridge the gap with a straight edge. A joint within the tolerance, or within the rounding of the two endpoint evaluations as the exact path allows, still closes. A centre-line path is open and is not joined back to its start (#368).
0.3.26 - 2026-10-29
Fixed
- A fixed-reference chain that ends at its start with a corner (a closing gap within the loop distance, a closing joint not tangent continuous) is refused by name; swept open, its two end caps crossed (#375).
0.3.25 - 2026-10-29
Added
pipe::fixed_reference_planandpipe::fixed_reference_along_piecessweep a profile with a fixed reference direction along a tangent-continuous chain of segments and circular arcs, open or closed, within the chord budget (#122). Where the reference is normal to the plane of every arc or lies in it, each arc is an exact revolution of the placed profile and each segment a prism, so the swept disk's per-piece proof carries over; a reference that only nearly is pays its measured twist of the exact frame off those stations out of the walls' half of the budget (FixedReferenceChain::twist), and an arc whose twist exceeds an eighth of the budget is reported as twisting (FixedReferencePlan::Twisting), which nothing here bounds. A corner (also one a re-seeded profile turns too far at, for a reference near the tangent), a reference parallel to the tangent, a gap and a profile that folds through a bend's axis are refused by name. A chain is a loop when the path declares itself closed or ends within the linear tolerance (and a quarter of the budget) of where it starts with a tangent-continuous closing joint. Alsoreference_clearance,ring_reachandpipe::check_disk_along, which refuses a disk wider than a lone arc's bend.
0.3.24 - 2026-10-28
Fixed
profile_ringsends every contour segment at its exact joint (#365). The first and last point of a flattened segment are its endpoints, not samples. Where two segments meet within the linear tolerance, both now produce one bit-identical vertex, the more exactly stated of the two: a line's endpoint at parameter 0 (its origin), a polyline vertex, any other line endpoint (origin + direction * trounds), a curved endpoint; a tie keeps the earlier segment's end. An arc ending on the revolution axis used to land atx ~ 6e-17(its end re-evaluated ascentre + r (cos s, sin s)), and a line ending there was rounded too, so a half-disc was refused at a full turn as a pinch and a partial turn meshed degenerate triangles. The snap distance is a rounding error (or a gap within the tolerance);profile_deviationadds it to its bound as it does a merged point. Ring vertices may move by an ulp wherever segments meet.
0.3.23 - 2026-10-28
Changed
revolvemeshes a profile touching an in-plane axis instead of refusing it (#363): a cylinder, cone or sphere from a rectangle, triangle or half-disk on the axis, and their partial turns, come out closed, consistently wound and free of degenerate triangles. A vertex exactly on the axis (the exact sign is zero) is one mesh vertex shared by every station and by both caps; its quads collapse to triangles and a profile edge along the axis emits nothing. The chord bound and the step count are unchanged. In a full turn a vertex on the axis with both ring neighbours off it (an isolated touch) is refused withUnsupportedInput: the solid pinches there and its boundary is not a two-manifold; a partial turn closes it into a sector point and meshes it. Vertices are welded only for an axis whose origin and direction have z exactly zero: with z noise within the tolerance a touching profile is stillUnsupportedInput, since its on-axis vertices are then off the real axis. A profile crossing the axis is stillInvalidInput.tapered_revolvekeeps itsUnsupportedInputrefusal for a profile touching the axis (#364).
0.3.22 - 2026-10-28
Fixed
revolveandtapered_revolve(the mesh path) refuse a profile whose vertices lie on both sides of an in-plane axis (InvalidInput), judged by an exact sign with no tolerance band; they used to sweep a self-intersecting solid and return it as valid. A tapered revolution judges the start and end sections together (#351).- A profile touching the axis is now refused (
UnsupportedInput) for full and partial turns alike, as the exact path does for full turns. This includes cylinders, cones and spheres revolved from a profile with an edge or vertex on the axis, and pie-slice partial turns, which used to mesh with degenerate triangles; meshing them properly is tracked in #363. An axis leaning out of the profile plane is still unchecked (#362).
0.3.21 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.20 - 2026-10-20
Fixed
section_lower::rectangle_contouraccepts a corner radius equal to its half-extent after unit conversion (#339). A radius above its limit by at most four relative epsilons of the smaller half-extent the limit is cut from is rounding and is snapped onto the limit, for the outer and the inner radius alike; one wider than that (a micrometre, say) is still refused by name. buildingSMART's IFC4 Annex Eair-terminal-element.ifctube (24 x 24 in, 2 in wall, 10 in inner fillet, in metres) used to be refused with "inner corner radius must lie in [0, 0.25399999999999995], got 0.254" and now builds with a fully rounded core. The structural sections have no such bound: their radii meet only the shared router's edge check, which already allows1e-12.
0.3.19 - 2026-10-16
Added
half_space::bounded_half_space_prism(#321): the slab of a half-space bounded by the prism of a boundary along any axis that crosses the plane, oblique to it allowed. The near cap is the boundary carried along the axis onto the plane, the far cap the same at the depth: aSlabDepth,Relativeto the boundary's extent (as before) or theReachof a subject.BoundaryPrismstates where the boundary stands (origin,x,y, used as given) and theaxis;BoundaryPrism::along_placement_zreads it from a placement, IFC'sIfcPolygonalBoundedHalfSpace.Position,BoundaryPrism::along_normalfrom a plane frame asbounded_half_space_in_framedoes, andBoundaryPrism::crossingthe cosine of the axis against the plane. Refused by name: an axis within the angular tolerance of the plane ("parallel to the clip plane"), a degenerate placement (axes parallel or zero) and a depth that is not positive and finite.BoundaryPrism::along_plane_normal(#322): a boundary placed by a whole placement along the plane normal, the placement'sxprojected into the plane, its scale, shear and handedness kept; a degenerate placement (parallel or zero axes) is refused by name.
Changed
bounded_half_space_in_framehonours a frame mirrored about the plane normal, whoseyis-(normal x x)(#322): its boundary is built mirrored, as written, still outward-facing. It used to rebuildyasnormal x xand build the mirror image. A frame standing perpendicular to the plane has no handedness there and is refused by name.
Fixed
- A bounded half-space whose boundary ring runs clockwise is built outward-facing, the ring read reversed (the same region); it came out inside-out before.
- A derived contour's arcs under a scaling transform are scaled once (#322): the scale went into the radius and into the arc's frame axes, so every arc came out scaled twice, its contour open, and the extrusion refused (
tests/derived_composite.rs).
0.3.18 - 2026-10-13
Added
loft_exact::loft_polygon_stations_exact(#247): an exact closed B-rep of planar faces joining planar polygon sections vertex for vertex, caps with holes and one planar wall per ring edge and span; a wall whose corners are not coplanar is refused by name (loft_exact::WARPED_WALL).loft::quad_deviationis public (#247): the bound a station run's mesh cells use for the wall quad against its two triangles.- Keyhole faces of a closed shell triangulate without a seam edge (#283, ADR 0083 amendment).
profile::triangulate_shell_faceistriangulate_with(.., PinchPolicy::Accept)for a planar face of a shell: no triangle edge lies on a seam (#270). Where the clipper would make the seam a triangle edge, the two triangles on it are replaced by the other diagonal of their quadrilateral when it is strictly convex, and otherwise by four triangles around a Steiner point strictly inside it, off the seam's line (a thin rim seamed at its corner always takes one). ASteinerPointis a chain of midpoints of the face's own points (the seam's midpoint halved towards a corner, one halving past the first that fits, as a margin against a caller's projection rounding), whichSteinerPoint::placereplays over any point type, so a caller lifting the face into space places it exactly in an axis plane. The result (ShellFaceTriangulation: pointsouter ++ holes ++ steiner, triangles, Steiner points) is certified against the cover it replaces: every triangle strictly counter-clockwise, every directed edge once, the same boundary, two more triangles per Steiner point, no edge on a seam; a seam no point can be placed off is refused by name. Rings without a seam triangulate exactly as underAccept. Tests (tests/profile_pinches.rs): every keyhole fixture from every start, either way round and under every zero-sign pattern, tiled exactly with no seam edge; the thin rim needs a Steiner point; the off-grid rim within rounding. Mutation probe:scripts/probe_keyhole_shell_mutants.py. - Swept disks mitre a corner beside an arc (#248). Such a corner was refused by name; it is now mitred at half angle like a corner between two segments, which is what the format defines. The mitre plane cuts the bend's torus in a different curve than the tube on its other side, so the end of the bend is meshed as a
pipe::MitreBand: from a square sectionbackshort of the corner, each profile vertex walks along its own circle of the torus to the plane (every band vertex on the exact torus), and a strip just short of the plane joins it to the ring shared across the mitre, meshing the planar face between the two sections.backandaheadare the least turns at which the section short of and past the corner clears the plane by0.01 r, from the closed form of the height's extremes over a section, so every circle of the torus crosses the plane once between them; a corner whose plane does not clear the bend's tube within a quarter turn, and bands needing more of an arc than it turns, are refused by name. A segment beside the arc keeps its own mitre ring, so its proof (#245) stands; the band and the strip are certified by the caller, whichpipe::pipe_planserves (PipePlan::arc_mitres,ArcMitre,MitreSide). - Swept disks round closed directrices (#248).
pipe::swept_disk_along_loopsweeps pieces whose last ends where the first starts: that joint is mitred, filleted or tangent like any other, and the walls close on the first station without caps. A frame carried round a non-planar loop comes back turned by the loop's holonomy (PipePlan::twist, in(-pi, pi]about the first tangent); the pipe turns its frames back by it uniformly over the loop's clear length (segments outside ther tan(theta/2)mitre zones, bends outside their bands), holding the frame in the zones so every mitre still shares one ring on both cylinders. The twist is inside the proven bound (the span bound measures it; a twisted segment gets stations until each span's twist fits, more than 4096 is refused withBudgetExceeded), and the last span pays its measured distance from the first station. A loop whose frame comes back turned with no clear length to take it is refused by name. loft::Stationandloft::FramederiveDebugandClone.
Documented
- A disk radius equal to a fillet or bend radius stays refused by name (#248), though the format permits it: the bend is a horn torus whose inner wall pinches to one point on its axis, a boundary that is not a two-manifold, which a closed mesh could only meet at a non-manifold vertex or misstate by standing off it.
0.3.17 - 2026-10-09
Fixed
- Oblique exact extrusions of profiles with arcs are the sheared prism (#280). For an offset leaning off the profile normal, the arc extruder (rounded rectangles, contours with arcs or round holes, sections with fillets, centre-line profiles, composites) built each arc wall as a right
Cylinderstanding on the profile plane and left the far rim's edges over the unsheared plan section, while the far vertices and cap were sheared: the solid was wrong by the horizontal shear (about 0.25 in the geometric audit for(0.3, -0.2, 1)at depth 0.75) and nothing refused it. Since #275 the downward case inherited it through the mirror. The far rim is now the base rim moved by the whole offset, and an arc sweeps the oblique circular cylinder it is: anEllipticalCylinderwith its axis along the unit directiond, semi-axesr(across the lean) andr d.z, and rims traced bySinusoid2pcurves (v = H / d.z - r |d_h| sin u), all closed form. Along the normal the walls stay right cylinders, unchanged.
Changed
An oblique circle extrudes exactly (#280) instead of being refused as
"oblique circle extrusion": its wall is the same oblique cylinder, its seam at the wall's angle origin. An oblique ellipse is still refused, as"oblique ellipse extrusion": its swept section has principal axes other than the profile's. Tests (tests/oblique_extrusion.rs): the issue's repro, and every family with arcs (rounded and hollow rounded rectangles, a round hole, a round contour, a 270-degree arc, a curved centre line, a circle, a derived and a composite profile, a filleted I section) along four leaning directions, up and down, each audited clean, of volumearea * depth * |d.z|(a closed form, or the straight prism of the same height), with far vertices the base ones moved by the offset and wall points that shear back onto the profile's circles; the downward family sweep now covers oblique curved families too. Mutation probe:scripts/probe_oblique_extrusion_mutants.py.Behaviour change: the mesh extrusion refuses a direction in the profile plane (#281), as the exact path does.
extrude_profilereturned a sliver solid for an offset within tolerance of the profile plane, and a flat zero-volume "solid" at exactlyo.z = 0; it now refuses|o.z| <= tolerancewith the exact path's typed refusal,UnsupportedInput { operation: Sweep, input: "extrusion direction in the profile plane" }.extrude, which takes no tolerance, refuses an offset exactly in the plane by the same name. A caller that relied on the degenerate mesh now gets the error;axiolid-mesh-compilemeshes extrusions throughextrude_profile, so it refuses them too. Test: both paths refusez = 0,-0.0and+-tol / 2, and build+-2 tolwith the same closed-form volume. Mutation probe:scripts/probe_oblique_extrusion_mutants.py.
0.3.16 - 2026-10-09
Added
profile::ring_touchesandprofile::RingTouch(#265): every vertex lying inside another ring edge, whichtriangulate_withunderPinchPolicy::Acceptinserts into that edge, found by the same exact validation and listed once per vertex and edge. A caller whose rings share their edges with neighbouring patches (the faces of a closed shell) splits the neighbour's copy of each such edge at the same vertex, so the patches do not meet at a T-junction.A ring joining its hole by a seam run both ways triangulates on surface paths (#270). Exporters write a planar face with a hole as one ring: the outer boundary, a seam to the hole, the hole, and the seam back (a keyhole). It bounds the outer polygon minus the hole, but the certified clipper refused it as "outer ring overlaps itself".
triangulate_with(.., PinchPolicy::Accept)now takes a seam -- an edge and a later edge of one ring between the same two points in opposite directions, used by no other edge, nesting with the ring's other seams and met by no other edge except at its ends -- out of the ring, splits the ring into the loops it joins and triangulates them on the pinch path (ADR 0083 amendment). The seam's two copies cancel, so the certificate over the loops is the ring's; its ends are repeated vertices and triangles use a point's first index. A hole loop of either winding is a hole, a seam between two loops side by side leaves two parts, and a keyhole may touch other rings at points. Refused by name: a "seam" running back along only part of an edge, one edge twice in one direction or three times, interleaving seams, seams in a row, an edge crossing, touching or running along a seam, and a loop folding back where its seam left it.profile::ring_touches(#265) validates such a ring the same way and lists its loops' touches by the ring's own edge and vertex indices.
Changed
triangulateandPinchPolicy::Refuse(extrusion and loft caps) refuse a ring with a seam as "profile outer ring runs along a seam both ways (edges i and j), which a solid cannot extrude" instead of "profile outer ring intersects itself" (#270): an extruded keyhole puts two coincident wall faces on the seam. Tests for #270: the issue's keyhole (hole wound either way), a thin rim with a 2^-13 m seam and one at 0.1 mm, a seam from inside an outer edge to a hole's corner, two keyholes in one ring, a keyhole with a pinch, a seam between loops side by side, each from every start vertex, either way round and under eight zero-sign patterns with the exact-tiling checks (the seam's copies cancelling in the edge cover), random keyhole layouts, every refusal above by name, and the solid refusals.
Fixed
Exact extrusions whose direction points against the profile normal build (#275).
extrude_profile_exactrefused any offset withoffset.z <= toleranceas"non-forward planar extrusion", so an opening cut down from a slab's top (ExtrudedDirection (0, 0, -1)) had no exact result and a boolean with it no certified mesh deviation. Such an extrusion is the forward prism along(o.x, o.y, -o.z)mirrored in the profile plane; it is now built that way, throughExactBRep::transformed, for every profile family and for oblique directions. Negatingzis exact, so the profile-plane cap keeps the profile's coordinates bit for bit, and the reflection flips every face, so the solid stays outward with the forward prism's positive volume. Only a direction within tolerance of the profile plane (|o.z| <= tolerance) is refused, now named"extrusion direction in the profile plane". The mesh path already built these solids; both now agree. Mutation probe:scripts/probe_downward_extrusion_mutants.py.A ring's orientation is no longer misread at a
-0.0twin of its lowest corner (#269). The certified clipper reads a ring's turn at its lexicographically smallest vertex, found withf64::total_cmp, which orders-0.0before0.0. A rectangle with a straight corner at(-0.0, 2)and its true lowest corner at(0, 0)was read at the straight corner, taken for clockwise and reversed, and both pinch policies then refused it with "found no ear". Projecting a face onto plane axes writes exactly such rings (0.0 * -k = -0.0next to the origin's0.0); a downstream triage found 35 refused rectangular faces in two models. Every coordinate comparison of the clipper now orders by value,-0.0equal to0.0(still total and deterministic): the lowest corner of a ring and of a pinch path's boundary cycle, the order of touching vertices along an edge, the bridge order of holes and the edge sweep.polyhedron::triangulateandoffset::offset_solidkey corners with-0.0read as0.0too, so a corner written both ways welds into one vertex. Tests: the issue's rectangle and a hole with the same corner under both policies, every pinch fixture under eight zero-sign patterns, random hole and pinch layouts with zeros negated at random, and a cube with a face written in-0.0.
0.3.15 - 2026-10-04
Added
- Rings that touch at single points triangulate on 2D and surface paths (#262). The certified clipper of #253 refuses rings touching at one vertex, which is right for a solid (the extrusion shares one wall edge between four faces) but refused valid regions a consumer re-feeds from overlay output: two rooms meeting at a corner, an L-shaped room wrapped round a column corner, a corridor eroded to a point, the same pinch reported through even-odd fill, a room minus door zones touching its walls.
profile::triangulate_with(rings, PinchPolicy)makes the choice explicit:PinchPolicy::Refuseistriangulate(extrusion and loft caps, unchanged),PinchPolicy::Accepttriangulates the region (ADR 0083 amendment). Two edges may then meet at one point that is a vertex of at least one of them; the vertex is inserted into the other edge, coincident vertices become one (triangles use a point's first index inouter ++ holes), each ring is split at its repeated vertices into simple loops turned by their nesting depth (inside an odd number of loops is inside), the loops' edges must alternate leaving and arriving round each shared vertex (else the rings cross there, refused by name), and each wedge there becomes one node, so the region falls into its connected parts -- two lobes of a pinch separate, a hole touching the outer ring merges with it, holes touching in a ring close off a part of their own -- each bridged and clipped as before. The certificate is the same over the split loops, with each visit of a pinch point counted as its own vertex andn + 2h - 2ctriangles forcparts. Rings touching nowhere triangulate identically under both policies; crossing and overlapping rings stay refused under both. Tests (tests/profile_pinches.rs): the consumer's cases (rooms at a corner, an L round a column, an eroded corridor, even-odd figure eights, a room minus door zones), footprints touching at a corner, holes touching the outer ring at a corner, inside an edge, at every corner and across the region, holes touching each other at a corner, inside a side, at their extreme corner and in a ring that closes off a part with its own hole, and an outer ring pinched round a hole, each from every start vertex and either way round, tile exactly (strictly counter-clockwise, exact area, no vertex on a triangle, untwinned edges covering every ring edge once) and are refused bytriangulateandextrude_profileby name; 300 random grid layouts of checkerboard squares and touching diamonds tile exactly, 200 off the grid tile, and layouts shrunk apart triangulate identically under both policies; crossing at shared vertices and overlapping rings are refused by name. Mutation probe:scripts/probe_ring_triangulation_mutants.py(52/52 killed).
Fixed
- Profiles with several holes extrude to closed solids (#253).
profile::triangulatedelegated toearcut, which drops nodes where the bridged ring runs straight on and lets only reflex nodes block an ear. For two holes side by side in one horizontal band (a 4 x 4 outer ring, 1 x 1 holes at x in [-1.5, -0.5] and [0.5, 1.5]) it returned 12 triangles of the right area instead of 14: one edge ran along the band's bottom line past both holes' inner corners, a T-junction, so the plain extrusion had 8 boundary edges and failed its volume and closure checks. Triangulation is now the crate's own ear clipper, every decision an exactorient2dsign (ADR 0083, superseding ADR 0015 for this crate): holes are bridged in order of decreasing largest x from their rightmost vertex, each bridge accepted only when it enters the polygon at both ends and touches no ring edge or earlier bridge, a vertex on it included; an ear is refused if any other vertex lies in its closed triangle. The output is certified before it is returned -- every triangle strictly counter-clockwise, every ring edge used once from inside, every other edge once in each direction -- and refused withDegenerateotherwise. Rings may come either way round; every vertex is now a triangle corner,outer ++ holesunchanged. Ring sets that bound no polygon with holes are refused withInvalidInputnaming the ring: a non-finite or repeated vertex, a ring folding back on or crossing itself, holes overlapping or touching each other or the outer ring (including at one vertex, which earcut accepted and which extrudes to a non-manifold edge), a hole outside the outer ring or inside another hole. The crate no longer depends onearcut. Tests (tests/profile_holes.rs): holes side by side, stacked, three on one ray, a hole vertex exactly on another's ray, five holes 1/1024 from the outer ring, and three flattened round holes in a row each triangulate with n + 2h - 2 counter-clockwise triangles, every ring edge once and every other edge twinned, cover exactly outer minus holes, and extrude to a closed, outward solid of volume area x depth; so do a triangle and an L with a straight or reflex vertex, from every starting vertex, as outer ring and as hole, a vertex in line with another hole's side just past its end, and 400 seeded random layouts of rectangles, triangles, L shapes, diamonds and rectangles with collinear midpoints on a quarter grid (compared with==) and 200 off-grid layouts of regular 3- to 40-gons; each invalid ring set above is refused by name; the certificate refuses earcut's output for the issue's profile. Mutation probe:scripts/probe_ring_triangulation_mutants.py(31/31 killed).
0.3.14 - 2026-10-04
Fixed
- Parametric profiles with decimal sizes lower at
Tolerance::ZERO(#250).contour_lower::contour_to_arc_ringdemanded bit-equal segment joints atTolerance::ZERO, which aLine2/Circle2contour cannot give for non-dyadic sizes: a line ends at a roundedorigin + direction, an arc at thecos/sinof its sweep. So every I section with IPE or HEA sizes (sharp or with root fillets), and the other families with decimal sizes, was refused before any boolean ran ("contour segments leave a gap of 2.6e-18"). The section router already computes each corner and tangent point once and hands it to both segments; the ring takes one vertex per joint (the leaving segment's start, a line's stored origin bit for bit), so it is closed by construction. The joint check now allows the larger of the tolerance and the rounding of the two evaluations meeting there (eight machine epsilons of the magnitudes they are computed from); a contour open by more is still refused atTolerance::ZERO. Rings are bit-identical to before wherever lowering succeeded, so results at a positive tolerance are unchanged. Tests: I (IPE 300/200, HEA 200, HEB 340; sharp, root fillets, toe radii), asymmetric I, T, U, L, Z, C, trapezium, rounded and hollow rectangles and an annulus lower at ZERO with each line's shared corner as its ring vertex, and extrude at ZERO to their closed-form areas, fillet terms(1 - pi/4) r^2included; tapered I/U/L/T lower and extrude at ZERO; joints far from the origin close to their own rounding; contours open by 1e-13 or 1e-9 are refused at ZERO. Inaxiolid-mesh-compile, an IPE 300 beam (with and without fillets) minus a round web hole clear of the fillets compiles at ZERO under exact placements with an empty report and the closed-form volume. Mutation probe:scripts/probe_section_zero_tolerance_mutants.py(9/9 killed).
0.3.13 - 2026-10-03
Fixed
- Restored: swept disks mitre their corners again (#245). Since 0.3.11 (#232)
pipe::swept_disk_along_piecesrefused a non-tangent joint as "undefined"; that was a regression (0.3.11 and 0.3.12). A disk swept round a corner between two straight segments is mitred at half angle, asIfcSweptDiskSoliddefines it: both tubes are cut by the plane that bisects the two tangents, and the two pieces share one ring on it whose every vertex lies on both exact cylinders (the outgoing frame is the least rotation of the incoming one, which agrees with the reflection in the mitre plane). The walls are planar trapezoids, exact faces of the ring prism; the ring is chorded to(c/2) cos(theta/2)for the sharpest mitre, because the mitre stretches the section by1 / cos(theta/2), so the exact tube stays within the chord budget (derivation in the module notes). Hollow disks mitre their bore the same way. The near-tangent tolerancejoint_toleranceis unchanged. Refused by name: a segment whose mitres reach past its length (L <= r |g_perp|,r tan(theta/2)for one mitre: the tube would cut through itself), a reversal, and a corner beside an arc (no ring lies on both the cylinder's and the torus's cut). - A disk radius equal to the fillet or bend radius is refused with its own reason (#245): the format rule permits a fillet radius equal to the disk radius, but the bend is then a horn torus whose inner wall pinches to a point on the bend's axis, which no closed two-manifold mesh bounds without meeting itself there.
0.3.12 - 2026-10-03
Changed
- A
Curve2::Chainprofile segment is named in deviation reports (arc-length chain profile segment with an unbounded piece) and in contour-lowering refusals (#239); a chain the flattener certifies is bounded by the chord budget like any other certified family.
0.3.11 - 2026-10-03
Added
pipe::swept_disk_along_pieces, withpipe::PathPieceandpipe::joint_tolerance(#232): a disk, optionally hollow, swept along a chain of straight segments and circular arcs, with every point of the exact tube within the chord budget of the mesh. Half the budget chords the disk; each piece is then bounded on its own by the #231 span bound (exact along an arc, where the stations are rotations about its axis, and zero along a segment), its end frames exact. Consecutive pieces share one station, so the tube is watertight and wound one way; the shared station's measured distance from the outgoing piece's own start station is added to that piece's first span. A joint turning by more than2 asin(c / (8 r))is a corner and is refused by name, as is a gap above a quarter of the budget, a bend radius at or below the disk radius, and a bend needing more than 4096 steps (BudgetExceeded). A fillet radius rounds each corner between two segments with a tangent arc (IfcSweptDiskSolidPolygonal); a fillet that overruns its segments, a disk at or above the fillet radius, a reversal and a corner beside an arc are refused by name. The derivation is in the module notes.profile::profile_deviationandProfileDeviation(#232): how far a profile's exact boundary may lie from the ringsprofile_ringsflattens it to. The chord budget for every segment family the flattener certifies, plus the largest merge of near-duplicate points, scaled by a derived profile's stretch; any other family (a clothoid, a non-positive spline weight) is unbounded by name.
Changed
- Contour lowering splits a circular segment of half a turn or more into equal sub-arcs below half a turn instead of refusing it (#228, ADR 0053 amended): an IFC arch is commonly one semicircle. The split vertices are the circle evaluated at their parameters, as a segment's ends are. Only a segment sweeping more than a whole turn is refused.
0.3.10 - 2026-10-02
Added
sweep::swept_disk_within,sweep::fixed_reference_sweep_withinandsweep::surface_curve_sweep_within, withsweep::SampledPath(#231): sweeps that take a directrix sampler instead of fixed samples, and keep every point of the surface their section traces along a smooth directrix within a chord budget. Each span is bounded by the directrix's sagitta, plus the section's extra sagitta at its furthest point from the directrix, plus the wall quads' twist; the directrix is resampled at half the budget until every span fits. Along a circular arc this is exact (the stations are rotations about the arc's axis); for other smooth curves it is the second-order estimate. A path with end tangents places its end sections square to the curve. A path without them (a line, a polyline, a composite) is swept as given.
Fixed
revolve::revolveandsweep::tapered_revolvebound the distance from every point of the surface their rings sweep to the mesh bytolerance.linear()(#231). The step round the axis used to bound the sagitta alone, which misses the twist of the walls when the axis leaves the profile's plane or the section tapers: the step now also covers each wall quad's distance from its two triangles (a proved bound, zero for the planar trapezoids of an ordinary revolution), and a tapered turn's spiral is bounded by its own curvature. The module notes carry the derivation, including why a chorded profile and a chorded turn each need their own share of a surface budget. A budget beyond 4096 steps is refused withBudgetExceededinstead of met by fewer steps.
0.3.9 - 2026-10-02
Added
- Exact swept disks (#223).
swept_disk_exact::swept_disk_along_line_exactsweeps a disk, optionally with a bore, along a straight segment into a capped cylinder;swept_disk_along_arc_exactsweeps it along a circular arc into a capped torus wedge, or a whole torus (with a toroidal bore for a hollow disk) for a full turn. Both are the exact extrusion and revolution of the disk placed withExactBRep::transformed. Volumes matchpi r^2 Land Pappus, every solid audits clean, and certified distances under tilted placements match their closed forms. Refused by name: a disk reaching the arc's axis, an arc beyond a full turn; invalid input: a non-positive radius, a bore not strictly inside the disk, a zero span or segment, a non-orthonormal arc frame.
Fixed
revolve::revolve(the mesh path) no longer builds an inside-out mesh for a negative sweep angle (#221). The station order it winds its walls from gave a positive signed volume only for a positive angle; sinceangleandaxis_directiononly matter through their product (the axis-angle rotation vector), a sign-of-anglecheck alone could not be made consistent with a sign flip in the axis direction too. The mesh's orientation is now settled from its own computed volume instead, so it matchesrevolve_profile_exact's positive sign for either sign of angle and either axis direction.tests/revolve_partial.rscompared only|volume|between the two paths because of this; it now compares the signed volumes.sweep::tapered_revolveshared the same unconditional station reversal asrevolve::revolveand so built the same inside-out mesh for a negative sweep angle (#221). Fixed the same way: orientation is settled from the built mesh's own signed volume rather than guessed from the station order.
0.3.8 - 2026-10-02
Added
- Exact partial-turn revolution (#172).
revolve_profile_exactnow builds a revolution through0 < |angle| < 2 piinstead of refusing it: every profile the full turn revolves (rectangles, circles, sections, contours with arcs and holes, derived, centre-line and composite profiles) becomes a closed exact B-rep whose walls are cylinder, cone, plane and torus patches trimmed to the swept angle, capped at both ends by the profile itself; each hole is a tunnel between the caps. Unlike the full turn, a partial turn may touch the axis: a vertex on it sweeps no arc (a cone closes at its apex, a planar wall becomes a sector) and a straight segment on it sweeps no wall, its edge shared by the two caps. The angle follows the right-hand rule about the axis direction, as the mesh path does; a section on either side of the axis is accepted. The newrevolve_partial::revolve_section_partialbuilds one lowered section. Volume (theta R A) and area (theta integral(r ds) + 2 A) match Pappus at 90, 180, 270 degrees and small angles (tests/revolve_partial.rs);scripts/probe_partial_revolve_mutants.pylists the faults the tests must catch.
Changed
revolve_profile_exactrefuses a turn beyond a full turn by name ("exact revolution beyond a full turn") and a zero angle as invalid input; both were previously reported as the partial-turn refusal, which no longer exists. A section crossing the axis, an arc whose circle reaches the axis, and an ellipse remain refused by name.
0.3.7 - 2026-09-30
Fixed
boolean_polyhedra_exactcompletes long chains of grid-aligned subtraction (#199). The depth-2 Menger sponge, 147 differences from a unit cube, refused at subtraction 82 with "every probe direction met a vertex or edge exactly"; it now finishes closed, with no face enclosing zero area, and with exactly the volume of its inputs. Two causes, both fixed at the root rather than by dropping fragments:- A split point is now the double nearest the exact crossing, not the result of
a + (b - a) * trounded twice. The old formula cut the planez = 1/3at0.33333333333333326, so two splits of one exact point landed ULPs apart and a later split through the pair emitted a ring enclosing no area. Correct rounding depends on the exact point alone, and a crossing that is a double comes back exactly. - A fragment one ULP wide has no double strictly inside it, so its f64 centroid rounded onto its own edge and was classified as the wrong point, leaving a hole in the shell. Such fragments are now classified at an exact dyadic interior point, with every predicate evaluated exactly. A fragment that encloses no area at all is refused by name ("a split fragment encloses no area") instead of failing later in ray classification.
- A split point is now the double nearest the exact crossing, not the result of
- Coplanar contact on slanted planes. A fragment was classified at its f64 centroid even when rounding put that point off the fragment's plane, so a face lying in the other solid's surface read as inside or outside it: a slanted tetrahedron united with itself came back with four faces whose windings disagreed, a silently broken solid. The classification point must now be certified in the fragment's plane (else the exact probe is used), and whether coplanar normals agree is an exact sign rather than an f64 dot product.
- The contact-matrix thin-overlap sweep (#200) passes at every overlap down to 1e-15: union, intersection and difference are closed and measure the exact slab. The boolean was closed all along; the test measured it with the 1 um
Tolerance::METRE, which calls a real 1e-12-wide face degenerate, drops it and reports the gap as a hole. It now measures withTolerance::ZERO, and the assertions are relative to the slab.
0.3.6 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.5 - 2026-09-28
Fixed
- Structural sections mesh (#193):
profile_ringsflattens everyProfile::Sectionfamily -- I, asymmetric I, L, T, U, C, Z, trapezium -- from the exact contoursection_contourbuilds, fillets and toe radii chorded within the budget, straight edges exact. It refused them withUnsupported { ProfileTriangulation }before. Rectangles with corner radii mesh with them, throughrectangle_contour: the mesh path used to drop the radii and mesh a sharp box. - Circles are chorded from half a step off the axes, not from angle 0 (#194): the same chords, turned, so a chord's middle, inside the circle, sits at every quarter turn. A circular void tangent to a face along an axis direction, as openings are, leaves a sliver of material under it instead of a chord point on the face, which pinched the solid.
0.3.4 - 2026-09-27
Added
bounding::minimum_enclosing_sphere(#118): the least sphere holding a point set, by Welzl's algorithm with exact in/out decisions (diametral, least sphere through three points, circumsphere through four), the centre enclosed from exact dyadic values and the radius rounded up, withSphereEvidence::errorbounding centre offset and radius excess.bounding::oriented_bounding_box(#118): a box holding every point, exactly (|(p - centre) . axes[i]| <= half_extents[i]for the returned doubles), never larger than the axis-aligned box. Tries the axis-aligned box, the principal axes, and each world axis, principal axis and exact hull face normal with the exact minimum-area rectangle across it. It does not claim the global minimum volume.- Depends on
axiolid-exactfor the exact decisions.
0.3.3 - 2026-09-27
Added
Exact full-turn revolution of circles, hollow circles, hollow rectangles and every section with holes (#111). Each hole revolves on its own and joins the solid as a void shell. Verified by Pappus on every case.
Composite profiles are unioned by
profile_lower::composite_regionsover oneArcArrangement(#111): members may carry arcs and their own openings, a member's opening stays open unless another member fills it, and members that do not touch become separate solids of oneExactBRep, for extrusion and revolution alike. Member vertices within the linear tolerance are welded first, so members authored to meet do meet.section_lower::circle_contour: a circle profile (and its bore) as exact quarter-arc contours. A circle moved off the origin by a derived profile now lowers to that contour instead of being refused.clip_arc_prism_exact(#120): an arc prism cut by a half-space whose plane passes between its caps, the "column under a sloped roof" case. Cylindrical walls stayCylinderfaces trimmed by an exactEllipse3edge with aSinusoid2pcurve (ADR 0071); planar walls get sloped edges; the cut cap is unnamed. A plane parallel to the axis is refused by name.boolean_prisms_exact_solidsandboolean_arc_prisms_exact_solids(#120): coaxial booleans whose result falls apart into separate pieces return one solid per piece, ordered by lowest vertex (x, then y), each audited on its own. An empty result is an empty list. The single-solid functions keep refusing a disconnected result, so callers that expect one solid are not silently handed the first piece.
Changed
boolean_arc_prisms_exactruns on the exact arc overlay (ADR 0070) and builds results it used to refuse: a result with interior holes becomes a solid with through-passages (#120), and a result starting abovez = 0is extruded from its own base height. Disconnected results go through the_solidsvariants.- Stepped coaxial booleans are built, not refused (#120, ADR 0072):
boolean_prisms_exact,boolean_arc_prisms_exactand their_solidsvariants return a union of prisms with different spans, a difference whose tool stops inside the subject (notch, counterbore, blind pocket, slot through the middle heights) as exact solids with their ledge faces. Walls are named after the operand edge they lie on, caps and ledges after the operand cap that made them. A result enclosing a cavity, and pieces touching only along an edge, are refused by name. clip_arc_prism_exactbuilds a plane that crosses a cap inside the section: the part of the old cap that survives keeps its name, next to the unnamed cut.boolean_steppeddocs: the bands are the lighter alternative to the stepped solid; their volumes are checked against it.
Changed
- Coaxial booleans whose result encloses a cavity return one solid with a void shell (#120) instead of refusing:
boolean_prisms_exact,boolean_arc_prisms_exactand their_solidsvariants. A cavity in a result of several pieces is still refused by name.
Fixed
A sharp rectangle revolved through
revolve_rectanglewrote its cap holes' pcurve intervals forwards although their uses run the edge backwards (ADR 0024), so each hole's pcurve ran against its edge: the geometric audit put it 8 off the edge and the solid measured 289 instead of 188.5. Found by the general boolean (#167); present before #125.Exact revolutions are no longer built inside out (#125).
revolve_profile_exactand the contour revolution put their surface frames at(x, y, z) = (X, Z, Y), which is left-handed; every loop is built anticlockwise in its parameters, so everyForwardface pointed into the solid. The topological and geometric audits compare faces with each other and passed it;exact_propertiesmeasured-2 pi R Afor every Pappus fixture. The frames are now(X, -Z, Y).
0.3.2 - 2026-09-25
Fixed
sweep::swept_diskcarries its section frame along the path by rotation-minimising frames (double reflection) instead of one fixed axis seeded from the first segment (#169). A pipe whose later leg ran along that axis was refused ("sweep reference direction must not be parallel to the directrix"), and a leg NEARLY along it projected the fixed axis to a residue of arbitrary direction, so the ring rotated between stations and the tube's volume collapsed with no error: in a real Revit rebar model 878 bent bars were refused and 6,644 compiled more than 0.5 % short, up to 64 %. The section is a circle, so the choice of perpendicular only rotates it about its own axis.fixed_reference_sweepis unchanged: there the reference is the author's.
0.3.1 - 2026-09-24
Fixed
extrude(and soextrude_profileand the reference mesh compiler) wound a solid inside-out when the extrusion direction pointed below the profile plane (direction.z < 0), e.g. an opening body extruded downward from its lintel (#166). The signed volume was-area * depth, so any boolean using the solid refused it as inside-out. Such a solid is now outward-oriented with the same magnitude, for outer and hole loops alike.half_space::bounded_half_space_in_framenow places the boundary at the authored frame's origin, projected onto the clip plane (#164). It used to take only the frame's axes and anchor the boundary at the clip plane's origin, so a boundary frame offset within the plane cut the wrong region with no error: the mesh stayed closed and correctly wound. The offset along the normal is still dropped, so the sweep starts on the clip plane and polarity and depth are unchanged.ReferenceMeshCompilerpassesBoundedHalfSpace.placement.translationas that origin, so compiled bounded half-spaces now honour the placement's translation as well as its rotation.
0.3.0 - 2026-09-23
Added
- Exact extrusion of rounded rectangles (
IfcRoundedRectangleProfileDef) and of hollow rectangles with outer and inner corner radii (IfcRectangleHollowProfileDef). Each corner is an exact quarter arc that extrudes to a cylinder wall.section_lower::rectangle_contourbuilds the contour through the same router the structural sections use (#111). - Exact full-turn revolution of a filled rounded rectangle; each corner sweeps a torus.
- Rounded and hollow rectangles are accepted as the basis of a derived profile, with the same similarity check every other contour goes through.
Changed
- Invalid rectangle radii are refused instead of clamped: negative, non-finite, wider than the half-extent, an inner radius on a filled rectangle, and a hollow section whose corners leave no wall.
axiolid-contracts
0.3.7 - 2026-11-07
Added
DisplacementBound, a finite non-negative distance bound that isEq, for the snap-rounding evidence of visible face regions (#410).
0.3.6 - 2026-11-03
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358). - The
MESH_VISIBLE_FACEScapability identifier (org.axiolid.geometry.mesh-visible-faces.v1) and theOperation::MeshVisibleFacesdiagnostic variant, for the visible face region contract (ADR 0094, #383).
0.3.5 - 2026-10-28
Added
- The
MESH_HIDDEN_LINEScapability identifier (org.axiolid.geometry.mesh-hidden-lines.v1) and theOperation::MeshHiddenLinesdiagnostic variant, for the hidden-line drawing contract (ADR 0090, #138).
0.3.4 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.3 - 2026-10-04
Added
ScratchRequirement::Affine { base_bytes, bytes_per_element, bytes_per_worker }(#226): at mostbase + per_element * elements + per_worker * workers. A purely per-element bound cannot be an upper bound for an operation with fixed setup cost or one that runs on a thread pool, whose workers each cost memory however small the input is. Additive: the enum is#[non_exhaustive].ScratchRequirement::upper_bound_bytes_on(elements, workers)andScratchRequirement::fits_budget_on(options, elements, workers), for a caller that knows the width the operation runs on.Parallelism::worker_bound: the most workers a preference allows (Serial1,Threads(n)n,Autothe available parallelism).
Changed
ScratchRequirement::fits_budgetcharges a per-worker term forParallelism::worker_boundof the options.upper_bound_bytesreportsNonefor an affine requirement with a per-worker term, which has no bound without a worker count. Existing variants are unchanged.
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-25
Added
ExecutionOptions::with_chord_errorandExecutionOptions::chord_error(#165): an explicit bound on how far a provider's straight chords may sit from the curve they replace, separate from the linear tolerance. The tolerance is a coincidence test; used as a chord budget it leaves a 5 mm arc a few chords atTolerance::MILLIMETRE, and small profiles mesh percent-level off.None(the default) keeps each provider's previous behaviour. A non-finite or non-positive budget is refused (None).
axiolid-core
0.3.5 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.4 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.3 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.2 - 2026-10-10
Added
- An optional
serdefeature, off by default, derivingSerializeandDeserializeforAabb,Frame2,Frame3,Interval,Plane3andBooleanOperator, withglam's ownserdesupport for the vector and transform aliases: the values the geometry graph's versioned wire format carries (#267, ADR 0085). Enums are externally tagged by their variant names and unknown fields are refused. No default-build change.
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-curve
0.3.12 - 2026-11-07
Fixed
QuadraticGraph2::heightis exact to rounding for either sign ofb(#414). It took the root as2c / (-b - sign sqrt(D)), which cancels for one sign ofb, losing about1e-11relative height near a vanishinga; it now takesc / qorq / awithq = -(b + sign(b) sqrt(D)) / 2, which adds terms of one sign.AngleGraph2::angleis unchanged: neara^2 + b^2 = c^2its loss is the problem's own sensitivity (the root moves with the square root of the discriminant), not a cancellation a different formula avoids.
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.11 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.10 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.9 - 2026-10-13
Added
- Curve-path piece bounds at a parameter or a point (#304, ADR 0082 amendment):
PathBound { Parameter, Point }and the fieldsPathPiece::start_boundandPathPiece::end_bound, set byPathPiece::between_parameters(curve, t0, t1),PathPiece::bounded(curve, start, end),PathPiece::with_startandPathPiece::with_end, leave a span's start or end distance to the evaluator, which resolves a point to its parameter and a parameter to its distance in the curve's station measure; so a caller without a provider can build a trim of an ellipse or a B-spline at interior parameters or points. Thestartorendbeside a bound holds an infinity, which nothing reads;PathPiece::lengthis infinite andPathPiece::is_statedfalse for a bounded piece (an offset over a bounded base included).
0.3.8 - 2026-10-12
Added
- Open curve-path ends (#298, ADR 0082 amendment):
PathPiece::open_end, set byPathPiece::to_end(curve, start)andPathPiece::whole(curve), leaves a span's end to the evaluator, which resolves it with its own length of the curve; so a caller without a provider can build a path over an offset beside a gradient curve, a clothoid or a B-spline (or a B-spline itself), whose lengths have no closed form.PathPiece::from_start(curve, end)is the span[0, end], andPathPiece::is_statedsays whether every length a piece spans (its offset's base included) is stated. An open piece'sendholdsScalar::INFINITY, unread, soPathPiece::lengthandCurvePath::lengthare infinite until an evaluator resolves it. Stated pieces are unchanged.
0.3.7 - 2026-10-10
Added
- An optional
serdefeature, off by default, derivingSerializeandDeserializeforCurve2,Curve3and every value they hold,SeamSideincluded: the values the geometry graph's versioned wire format carries (#267, ADR 0085). Enums are externally tagged by their variant names and unknown fields are refused. No default-build change.
0.3.6 - 2026-10-10
Added
- Curve paths (#290, ADR 0082 amendment):
path::CurvePath, pieces of curves laid end to end, each aPathPiece-- the span[start, end]of aPathCurve(an ownedCurve2orCurve3) in its station measure, traversed forwards or backwards, optionally carried by a rigid placement that is or is not exact. The neutral form of a composite, trimmed or segmented curve relation, which the curve-evaluation contract can see: it holds and composes (reversed,placed,extend,length,PathPiece::frame_is_exact) and measures nothing.PathCurveandPathPieceare#[non_exhaustive], so a later piece kind is additive. - Offset pieces of a curve path (#289, ADR 0082 amendment):
PathCurve::Offset(Box<PathOffset>), appended last;PathOffset { base, law }, an offset of one base piece measured in its own length;OffsetLaw::Planar(an offset curve 2D, left positive),OffsetLaw::Directed(an offset curve 3D, alongnormalise(V x T)) andOffsetLaw::Linear(offsets at stations, linear between the piece's ends, in anOffsetFrame), withPathOffsetsandOffsetFrame. All#[non_exhaustive].PathCurve::is_lineis also true for an offset of a line by a constant law, andPathPiece::frame_is_exactalso needs the offset's base placed exactly.
0.3.5 - 2026-10-09
Added
- Rotation about a held rail (#279, ADR 0081 amendment):
CantForm::AboutRail { rail, elevation }, a pivot piece whose elevation is derived from the cant law at the same plan distance,e = e0 + s D / 2 = e0 + s (b / 2) sin(psi),s = +1about the right rail and-1about the left (RailSide,RailSide::pivot_sign). A Viennese bend, a polynomial in the bank angle, now rotates about its low rail exactly; underBankConvention::VerticalRisethe held rail head standse0above the profile at every station. Built withCantPiece::about_rail;CantLaw::has_rail_piecesfinds one. Banked3::cant_rate_at:dD/dd, the law's own rate orb cos(psi) psi'for an angle piece.BankError::RailInCant: a held-rail piece in a cant law is refused by name.
Changed
Banked3::pivot_atreads a held-rail piece from the cant law: valuee0 + s D / 2and rates D' / 2, with the cant law's refusals at that station. Height pieces read as before; an angle piece in the pivot law is stillBankError::AngleInPivot.CantLaw::value_atandrate_athave no value on a held-rail piece.
0.3.4 - 2026-10-09
Added
SeamSide(#263, ADR 0082 amendment): which piece a position exactly on a seam of a composite curve is read from --Outgoing, the piece that starts there and what every evaluator reads (the default), orIncoming, the piece that ends there.#[non_exhaustive]; shared byaxiolid-model's stations andaxiolid-evaluate's station readers.
0.3.3 - 2026-10-03
Added
Curve3::Banked(Banked3)(#240, ADR 0081): an elevated centreline carrying a roll law, evaluated by plan distance.CantLawis a run ofCantPieces from plan distance zero, each aCantFormover its ownxi = s / length: a polynomial inxi(constant, linear, Bloss, and Helmert as two quadratic pieces throughCantPiece::helmert), a half-cosine, a sine transition, or a Viennese bend, which gives the bank angle itself (CantValue::Angle). A secondCantLawof height pieces is the pivot: the rotation point's elevation above the profile. The rail-head distancebgivespsi = asin(D / b)(bank_angle), and a cant beyond it is refused by name (BankError).BankConventionnames how a cant is read on a grade, with no default:TangentRotationrolls the section about the 3D tangent bypsi(the rail heads riseD cos theta),VerticalRisebyasin(D / (b cos theta))(they rise exactlyD).ElevationLaw::CircularArc { height, grade, radius }(#238): a vertical circular arc in plan distance, the circle itself rather than a parabola. Witht0 = atan(grade)and signedR(positive sag),sin t = sin t0 + d/R,z = height + R (cos t0 - cos t)andgrade = tan t, evaluated in a form that does not cancel for smalld/R.height_atandgrade_atanswer it in closed form and reportNonewhere|sin t0 + d/R| >= 1.ElevationLaw::Intrinsic { height, grade, curvature }(#238): a profile given by curvature against its own arc length (a linear law is the clothoid between grades). Stored exactly;height_atandgrade_atreportNonefor it, since its height is a quadrature, read byaxiolid-evaluate.ElevationLaw::circular_arc,ElevationLaw::intrinsicandElevationLaw::piece_at, the innermost piece of a composed law at a distance, rebased to its own start.Curve2::Chain(Chain2)(#239): a plane curve parameterised by cumulative arc length, its pieces placed rigidly end to end from a start frame.ChainPiece2::Intrinsic { curvature, length }is a curvature-law piece;ChainPiece2::Parametric { curve, start, length }reads anyCurve2, written in the piece's local frame (through the origin with tangent+xatstart), by arc length overlength. Each piece starts at the previous end point along the previous end tangent, so a chain is tangent-continuous at its joins: a layout such as line, cubic parabola, arc is one exact value.Chain2::is_well_formed,length,joinsandpiece_atread the data; evaluation lives inaxiolid-evaluate.Elevated3takes a chain as its plan, since its parameter is plan distance (ADR 0060).
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-27
Added
Field2::valueandSeriesField2::valueevaluate the value alone, without the jet, and agree withjetto the last bit.Bridge cells in
ImplicitCurve2(ImplicitCell::bridge, a cubic into a point where two branches cross, bounded by its Bezier control values),ImplicitCell::part,reversedandsolved_range,ImplicitCurve2::solve_cell, andField2::scale_at(the size of the terms that make up the value at a point, which its rounding scales with).Curve3::PairSection(PairSection3,PairNode): the section of two B-spline surfaces, carried by nodes on both and defined between them by the surfaces themselves (where both meet on the plane across the chord). It hassolve(the parameters on both surfaces and the point),rates,second_rates,sub,reversed,parameter_ofandside, andpair_section::solve4for the 4x4 systems behind it (ADR 0077).BSplineSurfaceis defined here, andaxiolid_surfacere-exports it unchanged, so a traced curve can carry a B-spline carrier (Carrier::Spline). It hasBSplineSurface::jet(point and first and second partials, rational) anddomain.Field2is now an enum ofSeriesField2(the former struct: powers and harmonics) andPatchField2(piecewise Bernstein polynomials on a grid, bounded by their coefficients over any box and continued past the grid by their edge polynomials).ImplicitCurve2::clippedcuts a curve to a box.Curve2::Lifted(LiftedCurve2): a space curve read in an analytic surface's parameters, sharing the curve's parameter. It is the pcurve, on the analytic face, of a section only a B-spline can carry.ImplicitCurve2::sub,rotated,reversed,shifted,closureandturning_points.implicit::{bound, bound_simple, partial}give interval bounds and partial derivatives of aField2over parameter boxes.Curve2::Implicit(ImplicitCurve2)andCurve3::ImplicitSection(ImplicitSection3)(#119, ADR 0077): a stretch of aField2's zero set in monotone cells, where each point is the field's unique root in its cell's bracket, and the same curve on its analyticCarrier(plane, ruled surface, sphere or torus).Curve2::QuadraticGraph(QuadraticGraph2)andCurve3::RuledSection(RuledSection3)(#119, ADR 0076): one root branch ofa(t) v^2 + b(t) v + c(t) = 0with degree-2 trigonometric coefficients (Trig2,Branch), and the same curve lifted onto a cylinder, elliptical cylinder or cone (RuledCarrier). The exact pcurve and edge of a quadric's cut across a ruled surface.Curve2::AngleGraph(AngleGraph2)andCurve3::TorusSection(TorusSection3)(#119, ADR 0076): the solutionu(t)ofa(t) cos u + b(t) sin u = c(t), and the same curve on a torus (TorusCarrier). The exact pcurve and edge of a plane's or sphere's cut across a torus.Curve2::Sinusoid(Sinusoid2): the graphv = mean + a cos(t) + b sin(t), the exact pcurve of a plane's cut across a cylinder in its (angle, height) parameters (ADR 0071). The parameter is the first coordinate. Additive:Curve2is#[non_exhaustive].
axiolid-curve-evaluate-contract
0.3.9 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.8 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.7 - 2026-10-13
Added
Joints near a window (#305, ADR 0082 amendment):
CurveEvaluator::path_joint_distances_within(path, around, window, tolerance), a defaulted trait method answering, as(joint, interval)in order, only the interior joints whose certified interval meets[around - window, around + window], each certified aspath_joint_distancescertifies it; a joint left out truly lies outside the window. A provider certifies only the pieces before and within the window: one piece whose length cannot be certified after it no longer refuses every placement on the path, while one before it, or within reach of it, still refuses by name. The default delegates topath_joint_distancesand keeps the joints that meet the window, so a provider that does not certify joints refuses it namingPATH_JOINT_DISTANCES_UNSUPPORTED.window_spanchecks the window (a centre that is not finite, a window negative or not finite:InvalidInput) andDistanceInterval::meetstests an interval against it. The conformance suite asks four lines turning at right angles and a widening offset then a line for windows on, between, over and past their joints: every returned joint must be certified and meet its window, every joint truly within a window must be returned, each must agree with the provider's whole-path answer, and a bad window or tolerance must be refused; a provider may refuse it as unsupported.Curve-path piece bounds at a parameter or a point (#304, ADR 0082 amendment): the trait documentation states how a provider reading paths resolves a
PathBound(re-exported), and names its refusals:PATH_POINT_OFF_CURVE,PATH_POINT_AMBIGUOUS, andPATH_PIECE_BOUND_UNSUPPORTEDfor a provider that reads stated spans only. The conformance suite reads a line from a parameter to a point and a circle from that point round its seam to a parameter, forwards and reversed, against closed forms, certifies its joint, and requires a point off its curve and a closed polyline's seam to be refused; a provider may refuse bounds as unsupported.
0.3.6 - 2026-10-12
Added
- Open curve-path ends (#298, ADR 0082 amendment): the trait documentation states that a provider reading paths resolves a piece's open end with its own length of the curve, and
OPEN_PATH_END_UNSUPPORTEDnames the typed refusal of a provider that reads stated spans only. The conformance suite reads an open path (the whole of a widening offset of the whole of a line, then a line to its end) forwards and reversed, refuses a distance past its end and certifies its joint atsqrt(101); a provider may refuse it as unsupported, typed.
0.3.5 - 2026-10-10
Added
- Certified joint distances of a curve path (#294, ADR 0082 amendment):
CurveEvaluator::path_joint_distances(path, tolerance), oneDistanceIntervalper interior joint that contains the joint's true distance (the sum of the true lengths of the pieces before it) and is no wider thantolerance; lengths read from stored data sum to point intervals. A defaulted trait method, so no provider breaks: the default refuses by a typedUnsupportedInputnaming the newPATH_JOINT_DISTANCES_UNSUPPORTED, never an estimate handed out as a certificate.DistanceInterval(lower,upper,#[non_exhaustive], built bynewandpoint) is new. - The conformance suite checks the query: a provider either refuses it as unsupported, or answers one interval containing the joint of a line and a quarter arc (exactly at 10) and of an offset of a line whose lateral offset widens from 0 to 1 over 10 (
sqrt(101), a quadrature for a provider), no wider than the tolerance, and refuses a non-positive or non-finite tolerance and a path whose pieces do not meet.
0.3.4 - 2026-10-10
Added
- Point, tangent and frame queries over a curve path (#290, ADR 0082 amendment):
CurveEvaluator::path_point_at_on,path_tangent_at_onandpath_frame_at_onread aCurvePath(re-exported fromaxiolid-curvewithPathPieceandPathCurve) as a composite station basis is read -- the distance end to end, every joint a seam read from the piece theSeamSidenames -- andpath_point_at,path_tangent_atandpath_frame_atare their outgoing reading;path_distance_conventionandpath_frame_is_exact_atsay which distance runs through the path and whether a frame on it is exact. All are defaulted, so no provider breaks: the sided queries refuse by a typedGeomError::UnsupportedInputnaming the newCURVE_PATH_UNSUPPORTED, the plain ones delegate to them, the convention isUnsupportedand nothing is claimed exact. - Conformance: a line and a quarter arc meeting at a right angle, forwards and reversed, on the joint and within the seam tolerance either side -- each side reads its own piece, the plain queries the outgoing one, a distance off the path and a path with a gap are refused, the convention is arc length and nothing on or after the arc is exact -- or every path query refused as unsupported.
- Re-exports
PathOffset,PathOffsets,OffsetLawandOffsetFramefromaxiolid-curve(#289): a curve path may carry offset pieces, which a provider that does not read them refuses by name.
0.3.3 - 2026-10-09
Added
- A seam side on point, tangent and frame queries (#286, ADR 0082 amendment):
CurveEvaluator::point_at_on,tangent_at_onandframe_at_ontake aSeamSide(re-exported fromaxiolid-curve) and read a measure on a seam -- a polyline's vertex, a grade break, a cant jump -- from the piece that ends there (Incoming) or starts there (Outgoing), by the station seam rule. They are defaulted, so no provider breaks:Outgoingdelegates to the side-less method, and every other side is refused by a typedGeomError::UnsupportedInputnaming the newSEAM_SIDE_UNSUPPORTED, never answered with the outgoing frame. - Conformance: both sides agree with the side-less answer off a seam and keep its refusals; at a polyline corner and a grade break each side reads its own piece, also within the seam tolerance for a provider that reads sides; a side a provider does not read is refused as unsupported.
Changed
CurveEvaluator::frame_atdocuments the axes the reference provider has always returned (#242):xthe tangent,yup,zto the right. The text used to callzup. No behaviour changes;axiolid-evaluatepins the axes on a level line, a grade and a banked curve.
0.3.2 - 2026-10-03
Changed
CurveEvaluator::frame_atdocuments that a curve carrying its own roll (Curve3::Banked, #240) is framed by it: its section frame, rolled about the tangent under the curve's bank convention (ADR 0081).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-decimate
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.2 - 2026-09-30
Added
- A test whose collapse passes the link condition and is refused by the normal-inversion guard, with
scripts/probe_decimate_mutants.pyshowing that dropping either guard turns the suite red (#201).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-decompose
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-dispatch
0.3.13 - 2026-11-13
Added
MeshHiddenLinesRegistryandMeshVisibleFacesRegistryroute bysupports_projection: a provider that does not support the view's projection is skipped, and a request no provider supports isUnsupportedbefore the topology audit. A perspective view needs anearbelow zero (InvalidInputotherwise).register_perspective_conformantregisters a provider after the base suite and the perspective suite.register_conformantof both registries also runs the perspective suite when the provider claims perspective, so an unverified claim is refused (ADR 0107, #387).MeshVisibleFacesRegistryacceptsFaceGroups::Planar: it checks that a region's group id is a triangle of the mesh and no later than any triangle the region lists, and does not recompute the groups (ADR 0106, #384).MeshHiddenLinesRegistryacceptsEdgeKind::GroupBoundarysegments only whenEdgeSelection::group_boundaryis set, and then refuses aPerTrianglegroup slice of the wrong length naming the mesh (ADR 0106, #385).
0.3.12 - 2026-11-03
Added
MeshVisibleFacesRegistry(featuremesh-hidden-lines): registration, conformance-gated registration, fallback onUnsupported/Unavailableonly, memory admission, and an outcome check (mesh provenance, rings, ascending triangles of the region's group, caps only for a solid with a near plane, order, limits and evidence). A group slice whose length is not the triangle count is refused naming the mesh (ADR 0094, #383).
Changed
- The shape, topology and memory checks that the hidden-line registry ran moved into helpers shared with the visible-face registry; the refusals are unchanged.
0.3.11 - 2026-11-03
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358). MeshHiddenLinesRegistry::auditlists every input mesh thathidden_lineswould refuse, in request order, without searching (#370). It shares the per-mesh audit withhidden_lines; refusals of the whole request stayErr.
0.3.10 - 2026-11-03
Changed
MeshPlaneSectionRegistry::section_requestno longer refuses cut regions returned beside aBothregion; it checks that eachBothregion equals one cut region, and carriesPlaneSectionEvidence::in_plane_crossingsinto the evidence it derives from the answer (ADR 0093, #366).
0.3.9 - 2026-10-29
Added
MeshPlaneSectionRegistry::section_requestchecks cut regions: a provider must answer them exactly when asked, with canonical rings, budgets and evidence, and noBothregion; a sheet request with cut regions is refused before dispatch (ADR 0092, #372). A provider that ignores the request and answers withcut_regions: Noneis treated asUnsupported, so the next provider is tried and a registry without one refuses.
0.3.8 - 2026-10-29
Added
MeshPlaneSectionRegistry::section_requestaccepts open meshes forSectionRole::Sheet: boundary edges are allowed, degenerate, non-manifold and inconsistently wound input is refused before dispatch, the evidence must echo the role and the auditedboundary_edges, and a solid answer must not end a chain on the mesh boundary, nor may any answer for a mesh without boundary edges (ADR 0091, #359).
0.3.7 - 2026-10-28
Added
MeshPlaneSectionRegistry::section_request: the source checks ofsection, fallback only on unsupported or unavailable providers, and outcome checks of the frame, chain and ring invariants, canonical order, budgets (in-plane regions, region vertices, in-plane pairs) and evidence counts (ADR 0091, #359).MeshPlaneSectionRegistry::register_request_conformantalso runs the region suite and rejects a claimed failure (#359).
0.3.6 - 2026-10-28
Fixed
MeshPlaneSectionRegistryrefuses a mesh whose index count is not a multiple of three withInvalidInput; it used to drop the partial triangle and could accept the rest as a closed solid.
Added
MeshHiddenLinesRegistrybehind themesh-hidden-linesfeature: shape, budget and topology checks before dispatch, outcome validation, fallback only on unsupported or unavailable providers, and refusals that name the mesh at fault (ADR 0090, #138).axiolid-hidden-linesis the first provider behind it. Adjacent pieces of one source edge must differ in visibility when hidden pieces are requested.
0.3.5 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.4 - 2026-10-14
Fixed
- The mesh-section registry uses the shared device matching instead of its own copy, so a build with only the
mesh-sectionfeature no longer warns that the shared one is unused, and the routing rules cannot drift apart.
0.3.3 - 2026-10-13
Fixed
- A one-thread context from
with_executionis charged one worker and runs the provider on a pool of one (#259). It installed no pool, so the call ran on whatever pool the caller was in, and the budget fell back to the options'Parallelism.
0.3.2 - 2026-10-04
Fixed
MeshBooleanRegistry::booleanbudgets both operands (#226). It counted the subject's triangles alone, so a provider's declared scratch was checked at up to half the input it would really be given;subtract_manyandunion_manyalready counted every operand.- A per-worker scratch term (
ScratchRequirement::Affine) is charged for the width of the pool configured withwith_executionwhen it has more than one thread, since every provider call runs inside it, and otherwise for the options'Parallelism.
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-evaluate
0.3.22 - 2026-11-14
Added
bound::ChainPositionTableandbound::SeamSide(#438): certified points, unit tangents and curvature vectors of an arc-length chain, read in any order, each with a bound on its distance from the exact one. The exact chain is the representation's: an intrinsic piece's points come from anIntrinsicPositionTable; a line, circle, ellipse or B-spline piece is read at the parameter whose certified arc length (chord panels) encloses the distance, the evaluator's inverse only a guess that is bisected when its enclosure misses; placements carry the position and the turn error from piece to piece. A parametric piece may start up toPIECE_FRAME_TOLERANCEoff its local origin: a read at a join takes the piece that starts there (SeamSide::Outgoing, what the evaluator reads) or the piece that ends there (SeamSide::Incoming).Nonefor a family without a certified arc length, a piece longer than its curve and a budget that runs out.bound::ElevatedPositionTabletakes an elevated or banked curve over a chain plan (#438; 0.3.21 returnedNonefor one): the plan is read from aChainPositionTable.point3_on(d, side)reads a join from either side,jet3_onreturns the point with the first and second derivatives (the plan's from the chain table, the profile's and the pivot's in closed form; over an intrinsic plan the curve's own, bit for bit) andjet_errorsbounds how far the three can be from the exact ones.
Changed
chain::chain_certifiesisfalsefor a chain with a parametric piece other than a line, a circle, an ellipse or a B-spline (a sinusoid or a polyline, #438): no certified arc length exists to charge its evaluated ends against, soflatten2no longer claims such a chain.bound::certifies_flattening3claims an elevated curve over a chain plan again, exactly wherechain_certifiesdoes (#438; 0.3.21 refused it, #393, because its chord bound under-reported the evaluated ends).
Fixed
chain::chain_chord_bound,bound::chord_bound2over a chain andbound::chord_bound3of an elevated or banked curve over a chain plan bound the distance to the chord of the evaluated ends (#438): the exact-chord bound plus the larger distance of an evaluated end from a certifiedChainPositionTablepoint, the lower end read as the piece that starts there, the upper end as the piece that ends there. They reported only the exact chord: 1.95e-5 over[116.4375, 116.5625]of a chain of a line andk = 0.01 sin(50 s), where the exact curve lies 2.835e-3 from the chord of the evaluated ends, and 0 for a point span whose evaluated point is 1.4e-2 off; a parametric piece that starts off the end of the one before it (up toPIECE_FRAME_TOLERANCE) is charged its jump.curve::flatten2andflatten3no longer accept a span of a claimed family (certifies_flattening2/3) whose chord bound isNoneon its sagitta alone (#438): the span is refined and, at the depth limit, refused withBudgetExceeded. A chain under a stretched start frame (stretch 2 or 4) had its table refuse reads near its own bound, because a rounding allowance grew with the square of the stretch; the resultingNonebounds passed as proven and the polyline lay 3.9e-3 from the curve at a 3e-3 tolerance.
0.3.21 - 2026-11-13
Added
bound::chord_bound2boundsCurve2::Liftedpcurves (#414): a space curve read in a surface's parameters,S(p(t)) = c(t). The implicit function theorem, with a certified separation of the surface's parameters over a box grown about the evaluated ends, bounds the pcurve's speed and bend per piece of the span (J p' = c',J p'' = c'' - H[p', p'],|J w| >= sigma |D w|); each evaluated end carries the outward-rounded residual of its image against the space curve over the separation. The space curve's own derivative bounds come fromcurve_derivative_bounds3, or through the carrier for ruled and torus sections and a cell of an implicit section. Analytic carriers (planes, cylinders, cones, spheres, tori) and the sections of two surfaces read on either of them (the same continuation on the four-dimensional system of the section, boxes clamped to a B-spline's knot domain) are covered. A pole, an apex, a fold, a B-spline crease, a guide that could change the turn inside a piece, a space curve without derivative bounds and a B-spline carrier outside a pair section are refused (None). Over a curved carrier the bound adds the magnitudes ofc''and of the carrier's curvature alongp', which nearly cancel for a curve that hugs it, so it is sound but not tight there. The mesh compiler samples pcurve edges at uniform spans, ignoring their breaks, and so still reports such trims unbounded until it cuts at them (#436).bound::chord_bound2boundsCurve2::Intrinsicbetween the evaluated ends (#414): the law's bound plus the larger distance of an evaluated end from a certifiedIntrinsicPositionTablepoint, with that point's own error.chain_chord_bounddoes not carry that error for the evaluated ends of the pieces of a chain yet (#438).bound::SurfaceBoundOracle::for_carrier(#414): the derivative bounds of the surface a curve lies on (axiolid_curve::Carrier, a general ruled carrier included), andSurfaceBoundOracle::separationwithParameterSeparation: a certifiedsigmawith|S(x) - S(y)| >= sigma |D (x - y)|over a parameter box, by Weyl's perturbation from the box centre, orNoneat a pole, an apex, a fold, a box too wide or a B-spline crease.bound::ElevatedPositionTable(#393): certified points of an elevated or banked curve over an intrinsic plan in plan distance, the plan's from anIntrinsicPositionTable, the height and the pivot in closed form, each point with a bound on its distance from the exact one.Nonefor a chain plan (TODO(#438)), an intrinsic profile, a held-rail cant, or an end past the plan.bound::IntrinsicPositionTable::max_error(#393): one certified bound on the error of every point the table returns.station::FrameLawbounds and charges a banked curve over an intrinsic plan (#396): the roll bounded from its cant law, each frame read compared with the exact rolled section (point from the position table plus the height and the pivot, axes from the heading and the roll), so the station, placement and fold paths decide it where they reported it unbounded. A held-rail cant and a banked curve over a chain plan stay unbounded by name (TODO(#394)).
Changed
bound::chord_bound2bounds the distance to the chord of the evaluated ends (#414). That differs from the exact chord only for the families whose evaluation is an estimate, which carry the difference: lifted pcurves and intrinsic curves.bound::chord_bound2of a point span (a == b) of a lifted or an intrinsic pcurve is the evaluated point's own certified error, no longer zero, andNonewhere the curve has no bound (#414).curve::flatten2accepts a span of a lifted or an intrinsic pcurve only when its certified chord bound is within the tolerance too (#414), where it relied on the midpoint sagitta alone;certifies_flattening2still names neither family, whose bounds are not tight on a curved carrier.bound::continuity_breaks2andcontinuity_breaks3name the cell joins of an implicit section and the interior nodes of a pair section (#414), where its parameter rate changes; a lifted pcurve breaks where its space curve does. A span across one is refused bychord_bound2, as across a B-spline knot.
Fixed
bound::chord_bound3of an elevated or banked curve over an intrinsic plan bounds the distance to the chord of the evaluated ends (#393), aschord_bound2does for the plan itself: the exact-chord bound plus the larger distance of an evaluated end from a certified table point. It reported only the exact chord, 1.95e-5 on a plan with a curvature0.01 sin(50 s)over[96.4375, 96.5625], where the exact curve lies 2.83e-3 from the chord of the evaluated ends (the plan point is 1.4e-2 off at 96.5;TODO(#480)). The bound of a point span (a == b) is that distance. A chain plan is unchanged (TODO(#438)), and so is an intrinsic profile, whichcertifies_flattening3does not claim.bound::certifies_flattening3no longer claims an elevated curve over a chain plan (#393,TODO(#438)): the exact-chord bound ofchord_bound3under-reports the evaluated ends there (1.95e-5 against 2.8e-3 on a chain of a line andk = 0.01 sin 50 s), soflatten3and the callers that trust the claim no longer take it as a certificate.
0.3.20 - 2026-11-03
Added
StationPiece::native_span,CompositeBasis::distance_interval_atandCompositeBasis::end_points(#424): the native parameters the kernel reads at a piece's start and end measures (so a graph compiler cutting a composite flattens each piece between the parameters a station reads), a certified interval for the length from the composite's start to the point read at a distance (a point where every length is stored data), and the points read at distance0and at the length.FrameLaw::bendandBendBounds(#361): certified bounds, over a seam-free span, of the coefficients of the signed bend of a section frame law,O'.N,L.N'andU.N'(Nthe frame's tangent), in the section and the plan reading. A section point moves along the plane normal atO'.N + long' - a L.N' - b U.N', so a caller can tell where the section planes stop advancing (a fold). Closed-form frames only: a line, a circle, a polyline's segment, an elevated or banked curve over a line or a circle, and an intrinsic curve; frames read by quadrature and every frame without bounds have no bend (None).FrameLawBounds::forwardboundsO'.Nfrom below for every frame with bounds.FrameLaw::has_bendtells a frame with a closed-form bend, which a narrower span bounds, from one without.station::FrameLaw::error_atandStationFrameError(#405): how far the frame read at ONE station lies from the exact frame law's, its point and each axis, for a part placed at a station on a curved basis. A frame read in closed form (a circle, a polyline, an elevated or banked curve over a line or a circle) is charged its rounding, an intrinsic curve's is compared with the exact one, and a distance resolved by the arc-length inverse, or after joints measured by quadrature, carries the exact frame to the one read through the derivative bounds.Nonefor a frame read by quadrature, a piece placed inexactly, a station within the resolution of a seam it is not on, and every familyFrameLaw::boundsleaves unbounded.FrameLaw::atomic_placed(#408):FrameLaw::atomicfor a curve whose placement may be inexact.FrameLaw::with_placements_read_as_exact(#408): bounds the frames of a piece placed inexactly as read, to decide what depends on them alone (a fold of the section planes);error_atstaysNonefor such a piece.bound::chord_bound2boundsCurve2::QuadraticGraphandCurve2::AngleGraphpcurves (#414), which it left unbounded. The rootw(t)ofF(t, w) = 0hasw'' = -(F_tt + 2 F_tw w' + F_ww w'^2) / F_w; interval enclosures ofF's partials (outward-rounded, from enclosed trigonometric coefficients) over the span by a certified height range bound it, on pieces of the span, andNoneis returned where the root may be undefined on the span orF_wis not bounded away from zero (a branch end nearby). Each computed end carries a certified root (a sign change ofFin outward-rounded intervals), and an angle graph whose height box reaches the+-picut ofAngleGraph2::angleis refused. Lifted pcurves (Curve2::Lifted) andCurve2::Intrinsicstay unbounded bychord_bound2; #414 remains open for them.
Fixed
FrameLaw::bounds,bend,has_bendanderror_atread the placement's exactness (#408): a piece placed in a frame that is not exact (a curve placed at a station on an arc) has no bounds, where its frames were certified against the read placement rather than the exact one.
0.3.19 - 2026-11-03
Fixed
IntrinsicPositions(#317) now charges the heading's rounding of a harmonic term of a curvature law (Sinusoid,Composite) on its own,8 eps |A/w| (2 + |p| + |w| L)per harmonic: the closed-form heading-(A/w)(cos(w s + p) - cos p)cancels, so for a slow or strongly phased harmonic its rounding is far above the64 eps (1 + K L)charged before, and a point could lie past its certified error.
Added
The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to thebound::IntrinsicPositionTableandMAX_TABLE_PANELS(#361): certified, random-access positions of an intrinsic curve (2D, or a planar space curve) over[0, end]. The composite two-point Gauss-Legendre rule on the closed-form tangent has the remainderw^5 / 4320 |f''''|, with `|f''''| <= sup|k'''| + 4 sup|k| sup|k''| + 3 sup k'^2 + 6 sup k^2 sup|k'|- sup k^4
per seam-free piece, so any curvature law with closed-form bounds ofktok'''qualifies. Checkpoints every 64 panels make any read cost at most 64 panels and independent of the order of reads, whereIntrinsicPositions` (midpoint rule) marches forward and restarts on a backward read.
- sup k^4
FrameLawbounds the frames of sections at stations along intrinsic curves (#361): a 2D intrinsic curve, a level planar intrinsic space curve and an elevated curve over an intrinsic plan (a gradient curve over lines, arcs and clothoids). The derivative bounds are the curvature law's closed-form suprema (|T'| <= sup|k|,|T''| <= sup|k'| + sup k^2, composed with the profile in plan distance), and each frame read is compared with the exact one (point from anIntrinsicPositionTable, axes from the closed-form heading) intopoint_errorandaxis_error.piece_frame_boundsstill returnsNonefor every intrinsic curve. Still unbounded: chains, banked curves over intrinsic or chain plans, intrinsic space curves with torsion or tilted, and curves with a stretched or skewed start frame.
0.3.18 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.17 - 2026-10-18
Added
- Chord bounds for constant offsets and intrinsic curves (#317), in
bound:offset_point2,offset_point3,offset_chord_bound2andoffset_chord_bound3evaluate and bound the offsetB + d uof a curve at its basis's parameter (uthe left normal in 2D,normalise(V x B')in 3D),sup |P''|from the basis's derivative suprema through the unit-vector rulestation::certifyuses for offset lengths (#294, #307), refusing a span across a break of the basis'sC^2continuity;intrinsic_chord_bound2,intrinsic_chord_bound3andintrinsic_derivative_bounds2bound an intrinsic curve from its curvature law (|c''| = |k|at unit speed, times the start frame's stretch), the chord bound across seams of a piecewise law.IntrinsicPositionsreads an intrinsic curve's points (2D, a planar 3D one in its start plane, and a 2D one's left offset) with a certified error: the unit tangent of the closed-form heading integrated by the composite midpoint rule, its remainderw^3 / 24 (|k'| + k^2)per panel from the law's bounds, panels split at seams, plus the heading's, the sums' and the placement's rounding;MAX_POSITION_PANELScaps the work.certifies_flattening2andcertifies_flattening3are unchanged. station::FrameLaw(#316, ADR 0082 amendment): certified frame-law bounds along an atomic curve or aCompositeBasis(CompositeBasis::frame_law), including pieces whose frames are read by quadrature, whichpiece_frame_boundsleaves unbounded: an ellipse, a B-spline, and an offset beside a line, a circle, an ellipse or a B-spline (constant, or by distances). Each is bounded against its exact frame law, the curve the length certificates measure, at its true arc length: every read frame is compared at its native parameter with the exact one, known in closed form to a certified error; the true arc length to it is bounded by chord panels over the piece, measured once; and over a span the law is bounded in its own arc length from the certified suprema of|P''|and|P'''|(the base's|B''|to|B''''|beside an offset, and the frame's unit vectors). A piece after joints measured by quadrature adds its start's distance from the certified joint to the measure. Intrinsic curves, chains, offsets beside them or beside elevated and banked curves, skewed circles, steep tangents, curves read by quadrature placed in a frame that tilts+Z, and placed offset pieces stay unbounded (None).FrameLawBounds::point_errorandFrameLawBounds::axis_error: how far a read frame's point and axes may lie from the exact frame law's, zero where the frame is read in closed form.
0.3.16 - 2026-10-14
Added
- Certified lengths of offsets beside a planar 3D intrinsic curve (#302, ADR 0082 amendment), which
station::certifyrefused namingUNCERTIFIED_LENGTHfor everyCurve3::Intrinsic. One whose torsion law is identically zero (Intrinsic3::is_planar, a structural test on the stored law) and whose start tangent and normal have nozcomponent (compared with0.0) is a 2D intrinsic curve lifted to a level plane: its offsets are certified by the midpoint rule on their speed in closed form, as beside a 2D intrinsic curve, measured in 3D. So the joints of a path of offsets beside a stand-alone composite with a clothoid lowered to 3D are certified. A torsion law not identically zero and a tilted plane are still refused by name; the message ofUNCERTIFIED_LENGTHnow says so.
0.3.15 - 2026-10-14
Added
- Certified lengths of offsets by distances along an ellipse or a B-spline (#307, ADR 0082 amendment), which
station::certifyrefused namingUNCERTIFIED_LENGTH. The law runs in the base's arc length, a quadrature, so the base is measured by chords first and the law at a native parameter is read through that certified length, a sub-panel to the point included; the measured curve is the offset whose law runs linearly in the base's true arc length between the points the kernel reads as the piece's ends, and a point's error from the table is carried by the chain of chords like rounding.P''andP'''are bounded from the base's derivative suprema to|B''''|and the law's derivatives along the base. So the windowed stations of an offset by distances beside an ellipse (openbimrs/ifc#443) resolve.
Changed
- Certified chord panels are refined adaptively, the widest first, until their widths sum to an eighth of the tolerance, instead of each being halved until within a uniform share (#307); the midpoint rule beside plan-parameterised bases too. Each chord panel bounds its deficit from both sides through the velocity across the chord: with
|P''|alone,M2^2 h^3 / 12over the speed (three times tighter than the turning bound, kept as a ceiling), and with|P'''|and the panel's own velocities to withinh^4, so an ellipse, a B-spline and their offsets certify to1e-8relative and below within the unchanged budget ofMAX_CERTIFIED_PANELS. A station 30 along the deck's offset certifies at a1e-5window's1e-8(refused before), and every interval the existing tests certify is narrower than before (by up to four times). UNCERTIFIED_LENGTHno longer names an offset by distances along an ellipse or a B-spline.
0.3.14 - 2026-10-13
Added
station::piece_frame_bounds,FrameLawBoundsandFrameReading(#247, ADR 0082 amendment): certified suprema of the first two derivatives, in station distance, of a station piece's basis point and section (or plan) frame over a span crossing no seam, for lines, polylines, orthonormal circles, and elevated and banked curves with a line or circle plan and a closed-form profile (a banked curve's roll bounded through its cant law), reversed and placed pieces included; with the slack of the arc-length inverse where the measure is read by it.Nonefor every numerically framed family.
0.3.13 - 2026-10-13
Added
Joints near a window (#305, ADR 0082 amendment):
CompositeBasis::joint_intervals_within(around, window, tolerance), andReferenceCurveEvaluator::path_joint_distances_withinthrough it, returns the joints whose certified interval meets[around - window, around + window], each bitwise asjoint_intervalsgives it (the same tolerance shares, summed in the same order). Every piece before the window is certified and its interval carried into the joints after it; no piece after the window is read, a joint being known past it once the certified lower end of the joint before it plus its piece's chord (the plan chord on a plan-measured piece) passes the window's end. So an uncertifiable piece (an offset by distances along a B-spline) after the window, or across its end far enough that its chord carries its own end past it, no longer refuses the query; one before the window, or within reach of it, refuses it namingUNCERTIFIED_LENGTH. The seam-snapping window's test of an interval against a window isDistanceInterval::meets, shared with it.Curve-path piece bounds at a parameter or a point (#304, ADR 0082 amendment):
StationPiece::from_path_piece, and soCompositeBasis::from_pathand everypath_*query ofReferenceCurveEvaluator, resolves aPathPiece::start_boundorend_bound. A point is resolved to the one parameter naming it (StationCurve::parameter_at_bound, modulestation::path_bound): in closed form on a line, a circle or an ellipse, by projection onto each segment of a polyline, by sampling and Newton on a B-spline, within the joint tolerance (point_bound_tolerance); a point off the curve, one several parameters name (a self-crossing, a closed polyline's or B-spline's seam) and one on a curve it does not invert are refused by name (PATH_POINT_OFF_CURVE,PATH_POINT_AMBIGUOUS,POINT_BOUND_UNSUPPORTED). A parameter is resolved to the measureStationPiece::between_parametersreads; a piece bounded at both ends IS that piece, a closed conic's span running round as a graph trim in its sense does, so it reads bitwise as the pathstation::curve_pathflattens such a trim into, and its joints are certified byjoint_intervalsas before. An end both open and bounded is refused.
0.3.12 - 2026-10-12
Added
- Open curve-path ends (#298, ADR 0082 amendment):
StationPiece::from_path_piece, and soCompositeBasis::from_pathand everypath_*query ofReferenceCurveEvaluator, resolves a piece whoseopen_endis set to the curve's end by this evaluator's own length, the endStationPiece::wholereads: a line's parameter domain, a length read from stored data exactly, otherwise the numerical estimate (a B-spline's or an ellipse's arc length, an offset's own table). An open path therefore reads bitwise as the stated path holding those lengths (CompositeBasis::pathhands it back), which is the path a graph compiler flattens;path_joint_distancescertifies where its joints truly fall. Stated spans are checked as before.
0.3.11 - 2026-10-10
Added
- Certified piece lengths and joint distances (#294, ADR 0082 amendment):
station::certify, andCompositeBasis::joint_intervals, oneDistanceIntervalper interior joint containing its true distance, no wider than the tolerance asked. A length read from stored data is a point; an ellipse's, a B-spline's and every numerical offset's is bounded from both sides. Where the measured curve is evaluated in closed form at its native parameter (a line, a circle, an ellipse or a B-spline, and any offset of one), by chords and turning: a chord is a lower bound, and withMa certified bound on|P''|(frombound::curve_derivative_bounds*and the frame's unit-vector derivatives) every tangent on a panel lies withinh M / (2 m)of the midpoint's,m = |P'(mid)| - h M / 2, so the arc is at most the chord over the cosine of that; the points' rounding is bounded once for the whole chain of chords. Beside a base parameterised by its plan arc length with a curvature law (an intrinsic curve or a chain, the plan of an elevated curve), by the midpoint rule on the offset's speed in closed form, withinh^3 / 24 sup |f''|from suprema of the curvature law's and the profile's derivatives. Uncertifiable lengths are refused by a typedUnsupportedInputnamingUNCERTIFIED_LENGTH(an offset of a banked or 3D intrinsic curve, of a chain's parametric piece or beside an intrinsic profile in a leaning frame; an offset by distances along an ellipse or a B-spline; a 3D offset direction that is not vertical beside a plan-measured or intrinsic base; a base placed in a tilted frame; a skewed frame), a tolerance the rounding already exceeds likewise, and a panel budget (MAX_CERTIFIED_PANELS) asBudgetExceeded. ReferenceCurveEvaluator::path_joint_distancesanswers withCompositeBasis::joint_intervals.- Seam-snapping windows (#294):
station::window, withsnapped_distance2,snapped_distance3andCompositeBasis::snapped_distance: the distance a station within its window of exactly one seam whose frame may jump is read at (that seam's own), using certified intervals where a seam's distance is numerical (toWINDOW_CERTIFICATIONof the window); a window holding two seams is refused namingSEAM_WINDOW_AMBIGUOUS, one with a seam straddling its edge namingSEAM_WINDOW_STRADDLED, a negative or non-finite window as invalid input. A window of0reads the distance as it is.
0.3.10 - 2026-10-10
Added
surface::invert(and solocate) andsurface::jettake an elliptical cylinder (#287): the angle is that of the section coordinates over the semi-axes, the inverse ofevaluate's affine image of the circle. They were refused as unsupported.projectstill refuses it: the closest point on an ellipse has no closed form.ReferenceCurveEvaluatorreads curve paths (#290, ADR 0082 amendment): the contract'spath_*queries buildCompositeBasis::from_pathand answer withCompositeBasis::section_onfor the same side, so a frame through the contract on a curve relation equals the same station lowered as geometry, bitwise against+Z; against another reference up the reference-up frame of the section's point and tangent (a banked piece refused there by name). A native parameter along a path is refused by name; the convention and exactness are the composite's.CompositeBasis::from_path(the path's curves borrowed, each piece checked against its curve, then measured asCompositeBasis::newdoes) andCompositeBasis::path;StationPiece::from_path_piece,StationCurve::from_path_curve, andFrom<StationPiece>/From<StationCurve>forPathPiece/PathCurve(the curve copied).Offsets as station bases (#289, ADR 0082 amendment):
station::offset, withStationOffset(an offset of one base piece by anOffsetLaw, measured in its own length: arc length, or its own plan length beside an elevated or banked curve),StationCurve::Offset(appended last),StationPiece::offsetandoffset_pieces(one offset piece per span of the base between seams, so every seam and joint of the base, and every break of a by-distances law, is a joint of the offset). A constant offset of a line is a line (frame exact), of a circle a circle of radiusr - a(length in closed form); every other offset is read numerically toOFFSET_TOLERANCEand never claimed exact. A collapse, a cusp or reversal (CUSP_TOLERANCE), a self-crossing in a horizontal plane, an offset of an offset and a base span across a seam are refused by name, as is an offset across a corner of its base, where its two sides do not meet.CompositeBasis(and sofrom_pathand the reference provider'spath_*queries) reads offset pieces, building each one's measure table once.
0.3.9 - 2026-10-09
Added
ReferenceCurveEvaluatorreads a seam side (#286, ADR 0082 amendment):point_at_on,tangent_at_onandframe_at_onanswer a measure ON a seam by the station rule (withinARC_LENGTH_TOLERANCE * max(1, s), for both sides) withstation_section3_on, so a placement framed through the contract agrees with the same station lowered as geometry; the frame is built against the evaluator's reference up, and a banked curve's against+Zonly, as side-lessly. Off a seam both sides are the side-less answer. A distance is located in the provider's convention, as is an elevated or banked curve's native parameter; the incoming side at a polyline's or a B-spline's native parameter is refused by a typedUnsupportedInput.- Banked curves rotating about a held rail (#279, ADR 0081 amendment): a pivot piece
CantForm::AboutRailis evaluated from the cant law, point, tangent and section alike.banked_second_derivativeaddse'' = s D'' / 2, for an angle pieces (b / 2) (cos(psi) psi'' - sin(psi) psi'^2).banked_derivative_boundsandbanked_chord_boundtake half the cant's bounds there, for a Viennese bend(b / 2) P_1,(b / 2)(P_2 + P_1^2)and(b / 2)(P_3 + 3 P_1 P_2 + P_1^3)withP_kthe bend's exact derivative suprema over the span (its ends and critical points), soflatten3and the sweep certification carry over.banked_breaksandgrade_corners3name the cant's seams inside a held-rail piece as the point path's. A cant law with a held-rail piece is refused by name.
0.3.8 - 2026-10-09
Added
- Composite station bases (#285, ADR 0082 amendment), in
station::composite:CompositeBasismeasures stations alongStationPieces -- spans of atomic 2D or 3D curves (StationCurve) between two distances in the curve's station measure, reversed or carried by a rigid placement -- laid end to end. The distance runs through the pieces in their common convention (plan distance when every piece is an elevated or banked curve, arc length when none is; mixed pieces refused by name); consecutive pieces must meet withinJOINT_TOLERANCE(relative), a gap and an undeclared reversed piece refused by name; every joint is a seam read by the #263 rule (section_onwith aSeamSide);seams/exact_seamslist the joints and each piece's own seams;frame_is_exact_atclaims an exact frame only where every piece up to the one read is an exactly placed line;pieces_betweenclips the pieces a trim of the composite keeps. SectionFrame::carried: the frame a station on a curve placed by a rigid motion has -- the source's moved where the motion keeps+Z(within the newKEEPS_UP_TOLERANCE), the placed curve's own reference-up frame of the moved point and tangent where it tilts+Zon an arc-length-measured source; an elevated or banked source so placed is refused by a typedUnsupportedInput.station::DistanceConvention, the curve-evaluation contract's convention re-exported so a caller can name a basis's measure.
0.3.7 - 2026-10-09
Added
Placing at a station (#264, ADR 0082 amendment):
SectionFrame::placementis the rigid motion taking localx,y,zonto the tangent, the left lateral and up and the origin onto the point (a linear placement's reading, not the provider layout ofSectionFrame::frame);SectionFrame::movedcarries a frame by a rigid motion;station_frame_is_exact2/station_frame_is_exact3say whether a station frame is exact (a line only: every other family reads its distance by the arc-length inverse or its point by quadrature).Seams of a station's basis curve (#263, ADR 0082 amendment), in
station::seam:station_seams2/station_seams3list where two pieces of a curve meet -- a polyline's vertices at the running sum of its segment lengths, a B-spline's corner knots (multiplicity at least its degree; by quadrature, flagged not exact), an intrinsic curve's and a chain's law seams and joins (smooth), an elevated curve's plan and profile seams, a banked curve's cant and pivot seams -- asStationSeam { distance, parameter, smooth, exact }, without evaluating the curve;exact_station_seams2/exact_station_seams3refuse an inexact seam with a typedUnsupportedInput.station_section2_on/station_section3_on: a station within the arc-length tolerance of a seam that is not smooth is read at the seam from the piece theSeamSidenames, the incoming one as the curve truncated at the seam (a polyline's previous segment, a B-spline's previous span, a profile and cant and pivot law cut there).Mitre: the plane a run of sections crossing a seam stands in, normal to the bisector of the two tangents;Mitre::betweenisNonewithinSEAM_TANGENT_TOLERANCEand refuses a near reversal (MITRE_TOLERANCEon the cosine of half the turn) and a seam whose sides do not share their point by name;Mitre::placeprojects a section point placed in each side's frame along that side's tangent onto the plane.
Changed
station_section2/station_section3read a station on a seam (withinARC_LENGTH_TOLERANCE * max(1, s)) at the seam itself, from the piece starting there. On a polyline or a B-spline the arc-length inverse could previously land a hair either side of the vertex, and so either frame.
0.3.6 - 2026-10-04
Added
- Elevated curves through the generic 3D curve functions (#252):
curve::evaluate3,derivative3,second_derivative3anddomain3read aCurve3::Elevatedby PLAN distance (ADR 0082, likeBanked3): the derivative is(p', grade), not unit, the second(k n, z''), the domain[0, L]over the plan's length (unbounded for a line plan, a full turn for a circle);second_derivative3also reads aCurve3::Banked(banked::banked_second_derivative, the pivot'se''added). elevatedmodule (#252):elevated_derivative,elevated_second_derivative,elevated_span, and the certifiedelevated_chord_bound--sqrt(P^2 + Z^2), the plan's Taylor chord boundh^2/8 sup |k|and the profile'selevation_chord_boundat equal plan distance, orthogonal parts composed in quadrature (the module documents the derivation and why the plan's ownchord_bound2does not compose) -- withbanked_chord_bound,elevated_derivative_boundsandbanked_derivative_bounds(sup |c'|, |c''|, |c'''|, plan and profile combined per law, a chain's parametric piece through its own derivative suprema),elevated_breaks/banked_breaks, andgrade_corners3, the seams across which the grade jumps, read from the laws on either side.bound::chord_bound3,curve_derivative_bounds3,continuity_breaks3andcertifies_flattening3dispatch to them, soflatten3certifies an elevated curve over a certified plan with a closed-form profile.
Changed
elevation: an intrinsic reading carries its profile arc length, andfrenet's pointwise curvature-law value is shared crate-wide (#252); no public signature changed.
0.3.5 - 2026-10-03
Added
station::SectionFrame::oriented(#246, ADR 0082 amendment): turns a section frame by an explicit axis and reference direction given as components in that frame,(tangent, lateral, up)-- the axis becomes the exact up, the reference direction is orthonormalised against it (Gram-Schmidt, axis primary), the point stays -- and refuses a zero, non-finite or parallel pair by name (station::ORIENTATION_TOLERANCE).bound::chord_bound2bounds implicit curves (Curve2::Implicit, ADR 0077) one cell at a time (#249), andcontinuity_breaks2names their cell joins. A regular cell's solved parameter is bounded through the implicit function theorem with interval bounds of the field's partials over the stretch's box (first and second order, the smaller kept); a bridge cell into a crossing by its cubic's Bezier control points. The deviation of a boolean whose exact result has traced pcurves (a hole through an I-beam's root fillets) can now be certified.station(#241, ADR 0082):station_section2andstation_section3resolve a distance along a curve -- plan distance on an elevated or banked curve, arc length (througharc_parameter) on any other -- to aSectionFrame: the point, the unit tangent, the lateral axis to the left andup = tangent x lateral. A 2D curve is framed inz = 0with+Zup, a banked curve by its rolled section, every other 3D curve by the reference-up frame against+ZthatReferenceCurveEvaluatorreturns.SectionFrame::placeapplies offsets,plangives the upright frame,framethe provider's layout (xtangent,yup,zright).station_length2/station_length3give the length a station is checked against; a negative, non-finite or too-long distance and a vertical tangent are refused by name.
0.3.4 - 2026-10-03
Added
banked(#240, ADR 0081):banked_point,banked_derivative,banked_tangentandbanked_sectionevaluate aCurve3::Bankedby plan distance. ABankedSectioncarries the rotation point, the unit tangent of the banked curve (its grade includes the pivot's rate), the lateral and up axes rolled about it, the cant, the nominal bank angle, the roll its convention gives, the pivot and the grade, withframe()andrail_heads(). Every refusal of the cant laws is passed on by name.evaluate3,derivative3anddomain3take a banked curve, parameterised by plan distance over its cant law's span, soflatten3flattens it;certifies_flattening3reports it uncertified (midpoint sagitta, #232).ReferenceCurveEvaluatormeasures a banked curve in plan distance, and itsframe_atreturns the curve's section frame. An evaluator built against a reference up other than+Zrefuses a banked curve, whose cant is measured against+Z.elevation(#238):elevation_heightandelevation_graderead every elevation law, alone or inside a piecewise one. Closed forms go throughElevationLaw::height_at/grade_at; an intrinsic (clothoid) profile is integrated withintrinsic_pointand its plan distance inverted by a bracketed Newton solve toINVERSION_TOLERANCE * max(1, d), after certifying that the profile stays below vertical over the bracket, so a profile that turns vertical is refused rather than read on a branch where plan distance runs backwards. Against a 50-digit reference a 150 m clothoid agrees to 4e-15 m in height.elevation_chord_bound: a certified bound on how far an elevation law strays from the chord of its heights over a span,h^2/8 sup|z''|withz'' = k / cos^3 t, for polynomials, circular arcs and intrinsic profiles;Noneacross a piecewise seam, outside a law's domain, or for a family it does not bound.arc_parameter(#239):arc_length2/arc_length3(signed arc length between two parameters of any evaluable curve) andparameter_at_arc_length2/parameter_at_arc_length3(the parameter at a signed arc length from a start parameter). Adaptive 8-point Gauss-Legendre of the speed, split at knots and vertices, and a bracketed Newton root find, both toARC_LENGTH_TOLERANCE(1e-12) relative tomax(1, length); lines, intrinsic curves and chains are exact. A length past the end of a bounded curve (named with the length available), a start outside the domain, non-finite input and a quadrature pastMAX_PANELSare refused by name. Against the binomial series of a cubic parabola the inverse lands within1e-13relative.chain(#239):chain_pointandchain_tangentevaluate aCurve2::Chainby arc length;evaluate2,derivative2anddomain2take a chain, and anElevated3over a chain evaluates by plan distance (elevated_point,elevated_tangent,ReferenceCurveEvaluator, and soCurve3::Bankedover it). A piece longer than its curve, a parametric piece off its local frame by more thanPIECE_FRAME_TOLERANCE, a malformed chain and a distance outside the chain are refused by name.Chains in the certified flattening (#232):
chord_bound2bounds a span inside one piece (an intrinsic piece byh^2/8times a bound on its curvature law, a parametric piece by its curve's own chord bound between the parameters the span reads),continuity_breaks2names the joins, andcertifies_flattening2accepts a chain whose intrinsic pieces have bounded laws and whose parametric pieces are certified families with no corner.
Changed
elevated_point,elevated_tangentandbanked_derivativeread the elevation throughelevation_height/elevation_grade, so elevated and banked curves carry circular-arc and intrinsic profiles.
0.3.3 - 2026-10-03
Added
bound(#232): certified derivative and chord bounds.chord_bound2/chord_bound3bound how far a curve strays from the chord of a parameter span (exact sagitta for an arc of at most half a turn,h^2/8 sup|c''|otherwise, and for a rational B-spline the projective formh^2/8 (|A''| + R|w''|) / w_min, which carries no first-derivative terms);curve_derivative_bounds2/3bound|c'|, |c''|, |c'''|of lines, circles, ellipses, sinusoids and B-splines (rational too) from closed forms and derivative control polygons;SurfaceBoundOraclebounds the first and second partials of every elementary surface and of B-spline surfaces over a parameter box, with interpolation coefficients for linear interpolation over a triangle;continuity_breaks2/3name the knots where a curve may fail to beC^k;certifies_flattening2/3name the families whose flattening is certified.
Changed
flatten2andflatten3accept a span only when its certified chord bound is within the tolerance too, for every familyboundcovers (#232): the midpoint sagitta alone let an ellipse or a spline bulge past its chord either side of the midpoint. A B-spline is cut at its corner knots (multiplicity at least the degree) first, so a corner is kept as an exact vertex. Families without a bound keep the sagitta test.
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-27
Added
Evaluation, derivatives and inversion of
Curve3::PairSection. ACurve2::Liftedreading a pair section on one of its own B-spline surfaces takes that surface's parameters straight from the solve.surface::locate,curve::locate2andcurve::locate3: parameters of a point, iterating where no closed form exists (B-spline surfaces and curves: seeded Newton, verified by the round trip).invert,invert2andinvert3keep their closed-form-only contract.Evaluation, derivatives and inversion of
Curve2::Lifted. AnImplicitSectionon a B-spline carrier is inverted through the surface'slocate.Evaluation, derivatives and inversion of
Curve2::ImplicitandCurve3::ImplicitSection(ADR 0077).invert2andinvert3now also coverQuadraticGraph,AngleGraph,RuledSectionandTorusSection, reading the angle off the point and trying whole turns.Evaluation, first and second derivatives of
Curve2::QuadraticGraph,Curve3::RuledSection,Curve2::AngleGraphandCurve3::TorusSection(#119, ADR 0076); a parameter outside the graph's spans is refused, not extrapolated.Curve2::Sinusoidevaluation: point, first and second derivative, a one-turn domain, and exact inversion (the parameter is the point's first coordinate, then its height is checked) (ADR 0071).
axiolid-exact
0.1.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.1.3 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.1.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.1.1 - 2026-09-28
Added
FixedInterval: a real number between two big-integer bounds at a chosen number of fractional bits, every operation rounded outward, andFixedInterval::sin_cos, certifiedsinandcosof a dyadic angle (Taylor series with the Lagrange remainder, after halving the angle). For signs of values no finite arithmetic holds exactly, asked again at a higher precision until they show (#181).
0.1.0 - 2026-09-24
Added
- First release (ADR 0068, #154). Filtered exact arithmetic for constructions:
Interval(outward-roundedf64, the fast tier),Dyadic(big-integer mantissa times a power of two, the exact tier), andcertify, which runs oneSignExprin the first and falls back to the second only when the interval cannot decide. Root2,sign_root,sign_two_roots: signs and comparisons of(a + b*sqrt(c)) / d, including across different radicands, by squaring with case analysis. Exact zeros are reported as zeros.- Constructions:
crossing_orientation(which side of a line two lines cross),line_circle_hits(missed, tangent or two hits, with tangency decided exactly),LineHit::{cmp_param, orientation}andcompare_along. TowerandNested: values with any number of nested square roots (capped at depth 6), with exact signs by recursive case analysis and an interval filter throughArith::sqrt_enclosure.IntPolyandRealRoot: exact real roots of integer polynomials by square-free reduction and Sturm sequences, isolated in dyadic intervals.- Conics:
Conic, exact line/conic and conic/conic intersection points with multiplicity, and the side of a line a conic point lies on. Arith::from_dyadic,Dyadic::enclosure(a soundf64interval; a point for values a double holds exactly),Dyadic::approx_parts,Interval::{quotient, disjoint}.
axiolid-exact-arrangement
0.1.2 - 2026-11-14
Added
faces,FacesandFaceId(#490, ADR 0109): the face on each side of every piece of an arrangement, nested components included, numbered from the unbounded face 0; withVvertices,Epieces andCcomponents the count satisfiesV - E + F = 1 + C.Nearestand, onExactPoints,nearest_on_segment,foot,compare_distanceandcompare_line_distance: exact distance predicates with no square root, decided through the interval tier andDyadic.
0.1.1 - 2026-11-07
Added
snap_round,Snapped,ClassBoundary,HOT_POINT_SCRATCH_BYTESandFRAGMENT_SCRATCH_BYTES(ADR 0100, #410): snap rounding of straight, pairwise interior-disjoint edges to the binary64 tensor grid. Each edge is routed through the centre of every hot cell it meets, in order, decided exactly (half-open cells, ties to even); the fragments are arranged and a crossing left among them is refused.Snappedcarries the displacement bound in the maximum norm and the number of rerouted edges, andSnapped::class_boundarynets a class's chain per fragment so that parts of zero width cancel.check_winding, which refuses a traced class that winds more than once at some point, andExactPoints::frame_sign.
0.1.0 - 2026-11-03
Added
- The exact point store, segment arrangement and ring tracing that the plane section of
axiolid-referenceused (ADR 0093, #366), as a crate of their own, so that the visible faces ofaxiolid-hidden-linesdecide on the same exact signs (ADR 0094, #383).
axiolid-exact-compile-contract
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-field
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-field-ops
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-fixtures
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-guarantees
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-heal
0.3.4 - 2026-11-04
Added
RepairAction::SplitTJunctionsandsplit_t_junctionssplit triangle edges at vertices lying on them within tolerance, so a mesh closed up to T-junctions becomes a closed oriented cycle. It adds no vertex and reports the deviation it accepted and the triangles it declined to split (#416).
Fixed
self_intersectionsdecides coplanar triangle pairs exactly at any finite coordinate scale: at about 1e154 and above an overlapping pair was reported as disjoint (and panicked in a debug build), because theorient2dit relies on overflowed. The fix is inaxiolid-predicates(#404).
0.3.3 - 2026-11-03
Fixed
self_intersectionsno longer reports disjoint, nearly coplanar triangles as intersecting after a rotation has rounded their vertices off their common plane. The triangle-triangle test was exact only in its side tests and fell back to a floatn_p x n_qdirection, which is noise for nearly parallel planes. Every decision is now a certifiedorient3dororient2dsign: an edge of one triangle against the closed other triangle, and for exactly coplanar pairs a separating-edge test on the exact projection (#399).- Coplanar triangles that overlap without a proper edge crossing or a vertex strictly inside the other (equal triangles, a vertex on an edge with shared area) are now reported; touching along an edge or at a vertex still is not (#399).
- Zero-area triangles are never reported as intersecting; the previous test reported a needle piercing another triangle.
diagnosestill flags such a triangle asDegenerateElement(#399).
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.0 - 2026-09-23
Added
- Repairs carry corner-indexed channels: weld leaves them untouched (they index values, not positions, so a seam is lossless), dropping and flipping triangles move their entries (#112).
Fixed
- Repairs keep attribute channels and normals in step with the geometry they rewrite (#114). Weld compacts per-vertex channels and normals; a seam drops the channel by name, a hard edge switches normals to corner-indexed. Dropping or flipping triangles moves corner-indexed normals with them.
RepairReport::attribute_fatesnames every input channel's fate.
axiolid-hidden-lines
0.1.5 - 2026-11-14
Added
ScalarHiddenLinesandScanHiddenLinesdraw perspective views and claim them throughsupports_projection, replacing the by-name refusal of 0.1.4: a pinhole at the frame origin, image(U / -W, V / -W),nearrequired and below zero. Every visibility predicate is an exact determinant of vectors from the eye, so triangles behind the eye need no case, endpoints are the exact rationals rounded once, and the near-plane cap is decided by ray parity along the eye ray. The perspective conformance fixtures and a binary64 eye-ray oracle (seeded scenes, eyes outside and inside) check it; the provider refuses a perspective view whosenearis missing, zero, positive or not finite withInvalidInput. The visible-face providers still refuse a perspective view by name (ADR 0107, #387).- The broad phase scales the depth coordinate of its hierarchy to the size of the image in a perspective view, where the image is in units of one over the depth and the longest-extent split would otherwise cut by depth alone. On the 50x50 perspective grid of
scenario/hidden-linesthe tree then returns 4.2M boxes instead of 44.3M before the exact test (540,676 pairs either way; the isometric grid returns 3.5M). The drawing is unchanged and orthographic views are untouched. A vertex at or behind the eye plane (W >= 0) has an unbounded image enclosure, so a triangle beside or behind the eye sends the whole scene to the linear scan (same pairs, slower candidate search); this is documented in the crate docs (#387). - An edge-on perspective triangle whose plane contains the eye may contain the eye itself, strictly inside, on a side or at a vertex. The eye in the closed triangle is decided exactly (no two of the three signs of
v_i × v_jalong the normal strictly opposite) and every ray from the eye then starts in the triangle, so its fibre reaches from the eye to the side the ray leaves by: the span of each non-skipped side counts as in front without the nearer-than test, while the near plane still has to be reached and the point still has to be strictly beyond it (ADR 0107, #387).
0.1.4 - 2026-11-13
Added
ScalarVisibleFacesresolvesFaceGroups::Planarexactly before labelling (newgroupsmodule: union-find over edges of exactly two triangles with parallel, same-facing normals, polled for cancellation, memory-budgeted; ADR 0106, #384).ScalarHiddenLinesdrawsEdgeKind::GroupBoundarywhenEdgeSelection::group_boundaryis set, resolving the groups only then; with it off the output is unchanged (ADR 0106, #385).The hidden-line and visible-face providers refuse a perspective view by name (
UnsupportedInput, inputperspective view) when called directly, and do not claim it throughsupports_projection(ADR 0107, #387).
Changed
- The providers' view-space vertices carry exact homogeneous coordinates
[U, V, W, h]withh = 1in an orthographic view, preparing perspective (ADR 0107, #387). The drawings are bit for bit the same: the segment hash of everyscenario/hidden-linescase is unchanged, and the wall clock there is unchanged (isometric and cut plan at 20x20 and 50x50 agree with the previous commit within run-to-run noise).
0.1.3 - 2026-11-03
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358). ScalarVisibleFaces, the exact provider forMeshVisibleFaces(ADR 0094, #383): the visible face regions of caller-grouped triangles in the view of the hidden-line provider, decided in the exact planar arrangement ofaxiolid-exact-arrangement, with region vertices that are exact points rounded once. Checked by the faces conformance suite, an independent binary64 ray-casting oracle and a mutation probe. Onscenario/visible-faces(a loaded machine, ranges over three runs) the BVH beats a full scan on a cut plan by 1.4-2.1x at 400 boxes and 4.1-6.6x at 2,500 boxes, and the depth prune of hidden edges saves 2.7-3.2x in an isometric view at 400 boxes; the BVH is not consistently faster than the scan in an isometric view or on interpenetrating boxes.
Fixed
ScalarVisibleFacesno longer refuses a scene asDegeneratewhen exact ring vertices round to the same binary64 value (#410, ADR 0100). The regions are the exact regions snap-rounded to the binary64 grid: an edge that passes through the rounding cell of another vertex is routed through it, and a region or hole whose area vanishes is dropped and counted. The evidence reportssnap_displacement,rerouted_edges,collapsed_regionsandcollapsed_holes; where nothing is rerouted the vertices are still the exact points rounded once. The two seeded oracle scenes that were refused (seed 1 oblique, seed 9 isometric, each with a near plane) are answered and pinned, with frozen copies in the conformance suite.
Changed
Request validation, the mesh audit and the building of view-space vertices, triangles and near-plane caps moved into one
preparestep that the visible-face provider will share (ADR 0094, #383). The drawings and the refusals are unchanged, except that a crease cosine outside[-1, 1]is now refused before the other request checks, not after the depth range.The rounding of an exact ratio to binary64 moved into
axiolid-exact-arrangement(round_ratio), so that the endpoints of hidden lines and the vertices of visible-face regions round alike. The drawings are unchanged.Hidden-line drawing does less exact work for the same output (#369): an edge stops looking at further candidates, and skips the near-plane caps, once one hidden span covers all of its kept part; a sign that a repeated vertex id makes exactly zero (
orient2dwith a shared vertex, a point on its own triangle's plane, a zero coordinate difference) is not evaluated; and a triangle whose plane is certainly not in front of the edge at either end, by the interval filter alone, is skipped before its three image clips. Visible face regions share this resolver, so theircandidate_pairsandexact_escalationsevidence and their candidate-pair budget fall the same way; the earlier visible-face timing figures predate this change. The drawing's segment hash is unchanged on everyscenario/hidden-linescase. Examined candidate pairs and exact escalations fall, as the early exit stops counting pairs: isometric 20x20 125,577 to 79,064 pairs and 187,483 to 43,402 escalations; cut plan 20x20 23,680 to 14,240 pairs and 113,440 to 51,200 escalations; isometric 50x50 829,935 to 511,016 pairs and 1,215,313 to 287,386 escalations; cut plan 50x50 148,000 to 89,000 pairs and 709,000 to 320,000 escalations. A loaded machine, three runs each (the BVH provider, ranges of the three estimates): isometric 20x20 0.73-0.85 s to 0.35-0.47 s, cut plan 20x20 0.17-0.48 s to 0.08-0.11 s, isometric 50x50 3.9-6.2 s to 1.9-2.6 s, cut plan 50x50 1.0-2.3 s to 0.51-0.79 s. Single runs vary by more than the gain on some cases, so read the work counts as the firm result. Because fewer pairs are examined, amax_candidate_pairslimit that the old count reached may no longer be reached.
0.1.2 - 2026-10-29
Changed
- Candidate triangles for an edge, and the cap triangles of a cut solid, come from an
axiolid-spatialBVH over view-space boxes instead of a linear scan. The predicate, the candidate set, its ascending order andcandidate_pairsare unchanged (#138). Wall clock fromscenario/hidden-lines(cargo bench -p axiolid-benchmark --bench scenario -- hidden-lines, which times the BVH and the linear scan,ScanHiddenLines, on each case; a loaded shared machine, so read these as ranges), linear scan / BVH: isometric 20x20 boxes 412 / 389 ms (a reviewer's run: 459 / 426), cut plan 20x20 174 / 140 ms (117 / 100; very noisy), isometric 50x50 3.34 / 2.52 s (3.62 / 2.88), cut plan 50x50 1.59 / 0.76 s (1.67 / 0.82). The gain is small at 400 boxes, because exact evaluation of the candidates dominates there, and 25-30% (isometric) to about 50% (cut plan) at 2,500.
0.1.1 - 2026-10-28
Changed
- The near-plane caps of closed solids occlude (#138, ADR 0090): a
Solidthat the near plane cuts no longer fails asUnsupportedInput. A point withW < near(farther than the plane) is hidden when the point of the plane above it lies in the solid, by exact ray parity with a symbolic perturbation of the image point. A face of a cut solid in the near plane is drawn, not refused.
0.1.0 - 2026-10-28
Added
ScalarHiddenLines, the exact scalar provider forMeshHiddenLines(ADR 0090, #138): boundary, silhouette and crease edges of triangle meshes split into visible and hidden pieces by exact interval and dyadic decisions, with endpoints that are the exact points rounded once. A near clipping plane that cuts aSolidmesh is refused asUnsupportedInputnaming it.
axiolid-inspect
0.3.10 - 2026-11-13
Added
thin_sheetandThinSheet: the first pair of triangles of one mesh that face each other within a linear distance on one plane and overlap in more than an edge. A boolean whose operand lands a rounding residue off the face it should meet leaves such a lid in a Solid (#400).
0.3.9 - 2026-11-04
Added
near_coplanarandNearCoplanar: the first pair of triangles of two meshes that nearly share a plane within a linear distance, not exactly coplanar. Placement leaves such a residue where faces meet in exact arithmetic; a boolean that decides on the raw values cannot settle it (#391).certify_unit_windingandWindingError: bracket the winding excessE = integral of w(w-1)/2 = (integral of w^2 - integral of w) / 2of a closed oriented cycle, an upper bound on the volume where its winding number is neither 0 nor 1 (equal to it wherewis in {-1..2}), and accept it within a positive budget. Edge-touching shells and cavities pass; duplicated, overlapping and inverted shells are refused, socycle_volume_propertiescan be trusted as the enclosed volume (#420).
0.3.8 - 2026-11-03
Added
The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).surface_gap: the distance between two meshes' surfaces with no containment test, so a mesh in another's cavity is as far from it as the surfaces are.min_gapdelegates to it after its containment check (#356, #357).
Fixed
min_gapand clearance reported a positive gap for meshes that cross with no vertex inside the other (a bar through a box, a pipe through a wall). An edge crossing a face is now a certifiedorient3dtest and reads asSome(0.0)(#390).
0.3.7 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.6 - 2026-10-14
Fixed
intersection_volumeanddifference_volume(#313) no longer collapse to the trivial[0, V]for bodies that touch flush within rounding. A face that is vertical but for a rounding residue casts a sliver of shadow whose plane has a slope near1e30, and its term's enclosure spanned more than either solid. Each term is now also intersected with the a priori bound[0, min(area f', area g') * (min(zmax_f, zmax_g) - base)], which holds however the arithmetic is conditioned. On a sink cabinet standing in its room, from the public buildingSMART IFC4 building-architecture sample, the shared volume goes from[0, 0.8008000000000190]to a bracket5.9e-14wide, and the difference from[0, 0.8008]to[0, 5.9e-14].
0.3.5 - 2026-09-30
Added
topology(#144):ComponentTopology::homology_basisnow also covers a closed non-orientable component and any component with boundary, orientable or not, not only the closed orientable case --kgenerators forkcrosscaps, and2g + (b - 1)ork + (b - 1)withb >= 1boundary loops, all withZ2(GF(2)) coefficients. Built by the same tree-cotree construction, generalised: it never needed orientability, and boundary edges now attach to a virtual dual node per boundary loop so every edge has a well-defined dual side. Still not provided: homotopy questions.
0.3.4 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.3 - 2026-09-27
Added
topology(#144): every connected component of a two-manifold triangle mesh, classified exactly from its connectivity -- counts, Euler characteristic, boundary loops, orientability and consistent winding, and the surface (SurfaceKind::Orientable { genus }orNonOrientable { crosscaps }). A closed orientable component also gets a basis of its first homology:2gsimple closed edge loops, by the tree-cotree construction. Meshes with an edge on three or more triangles, or a vertex whose triangles form several fans, are refused (TopologyError::NonManifold).genusis unchanged.
0.3.2 - 2026-09-27
Added
detect_planes(#131): the planar regions of a triangle mesh, grown over shared edges within an angle and a distance (PlaneTolerance) and then certified. EachDetectedPlanegives its triangles, a point and unit normal, adeviationthat is a proven upper bound on every member corner's distance from that plane (regions are peeled until it is within the requested distance), whether the region is exactly coplanar (byorient3d), and its area. Largest first; deterministic.
0.3.1 - 2026-09-27
Added
intersection_volume,difference_volumeandenclosed_volume(#183): the volume two closed triangle meshes share, the volume of the first outside the second, and a mesh's own volume, each as aVolumeIntervalcertified to contain the true value and never negative. No boolean is built: every face pair whose shadows overlap contributes the integral of the lower of the two planes over the overlap, with sides of lines and the lower plane decided exactly and the arithmetic rounded outward. Coplanar faces and touching bodies are exact cases. Open, non-manifold, self-intersecting or non-finite meshes are refused with the operand named (OverlapError); either winding is accepted.
axiolid-levelset
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-linear
0.3.5 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.4 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.3 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.2 - 2026-10-10
Added
- An optional
serdefeature, off by default, derivingSerializeandDeserializeforLineandPolyline: the values the geometry graph's versioned wire format carries (#267, ADR 0085). Enums are externally tagged by their variant names and unknown fields are refused. No default-build change.
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-linear-intersection
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-measure
0.3.15 - 2026-11-13
Changed
DistanceBounds::upper(all distance and clearance queries, plan and body variants) is at least the distance between the boundary points the witnesses stand for, not their computed distance: it adds their evaluation rounding, 16 ulps of each side's witness scale. A plan overlap still reports[0, 0](#439).
Fixed
- At georeferenced coordinates
Clearance::Belowcould be claimed within about1e-8of a limit the distance does not go below (#439). - The exact Hausdorff lower bound takes off its witness's rounding (a face charted from far away but lying near the origin raised it up to about
1e-9past the distance), and a point query's upper adds its nearest point's (#439). - B-spline witnesses are padded by the de Boor evaluation's rounding, a bound that counts the degrees and the weights' spread (#439). A B-spline face whose weight ratio exceeds
1 / (64 L eps)(Lthe sum of its degrees, about3.5e13for a bilinear patch) is refused withExactMeasureError::NonPlanarFace, since that bound is first order. - The exact Hausdorff upper bound about a B-spline patch's witness adds the distance from the witness to the centre of the patch's enclosing sphere (the control net's box midpoint), which it left out and so could report an upper bound below the distance (#459).
- The exact boundary distance and Hausdorff distance carry rounding margins derived from the magnitudes their numbers are computed from, in ulps of the coordinates, instead of
1e-12of them. At georeferenced coordinates (450000, 5600000) the old margins left an interval about1e-5wide that never closed to the accuracy asked; a search now settles at a floor of 128 ulps of its scale, about3.2e-7there and6e-14at unit scale, and an accuracy below the floor is never reported as met (#318). - The distance search closes gaps along a planar face's normal on certified foot witnesses (the foot of a witness on a planar face, the closest points of two line edges) instead of waiting for patch centres to line up, and the point queries of the Hausdorff search pop equal bounds first in, first out (#318).
- The margin for taking two boundary points as touching is 128 ulps of their magnitudes, not
1e-12of them: items of one body apart by more than that are measured instead of refused asItemsNearlyShareFace, and at unit scale a1e-13gap is now a gap (#318). - The sphere and torus branches of a projected range carried no rounding pad at all (#318).
- The Hausdorff matched bound, a translate's re-charted surface and cone pole, and the displacement gate use the same derived margins, and a plane, cylinder or elliptical cylinder is Lipschitz by its axes rather than
1 + |v|times as steep, which had inflated the bound of patches charted in world coordinates (#318).
0.3.14 - 2026-11-04
Added
cycle_volume_propertiesmeasures the signed volume and centroid of a mesh that is a closed oriented cycle (every edge used equally often in each direction), such as shells that share an edge. It is bit-identical tovolume_propertieson two-manifold input and refuses anything open withCycleVolumeError(#416).select_by_normalandextent_alongselect the faces of a mesh whose normal lies in aNormalCone(the tolerance's angular part is slack on the half-angle) and measure their area and their extent along a direction. Selecting every face reproducessurface_propertiesarea bit for bit (#417).The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.13 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.12 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.11 - 2026-10-20
Fixed
exact_propertiesmeasures a solid far from the world origin correctly (#343). Its cone fields were integrated about the world origin, so on a national grid each term grew with the cube of a5.6e6m reach and the noise floor of every curved-face integral was set from that reach: a0.57 m^3box at(6.0e5, 5.6e6, 204.5)measured0.544to0.664 m^3depending on the last bits of its placement, a wall less flush openings up to 5 % off, each without an error. The solid is now moved to the centre of its vertices' bounding box and integrated there, and the centroid and second moments (still about the world origin) are shifted back by the parallel-axis terms. Such a solid measures its closed form to its input's own grid (9.3e-10m at5.6e6); one at the origin measures as before up to rounding. A B-rep the translation refuses (an elevated or banked alignment curve) is integrated in place about the same point: a curved face far out may then be refused as not converged, never measured looser.second_moments(and soMeshMeasure'ssecond_moment_diagonal) sums about the mesh's first vertex and shifts the moments back to the origin by the parallel-axis terms, asvolume_propertiesalready did (#343). About the world origin each term grew with the fifth power of the distance: a unit box at(6.0e5, 5.6e6, 204.5)measured360016800160against360016800196.08and lost its central moments entirely. It now matches the closed form to rounding.
0.3.10 - 2026-10-09
Changed
boundary_distance,boundary_clearanceand thebody_*distance and clearance in space stop as soon as the two boundaries are shown to meet (#273): a pair the search pops with a lower bound of zero is first asked for a point of each element that the two share -- the closest points of two line edges, or a point of a line edge and the point of a planar face at the same place, the face's certified by its domain. Two such points within the rounding margin the lower bounds carry give[0, d]with them as the witnesses, the interval the refinement would only converge to. Touching or crossing elements no longer refine round the contact until the accuracy or the step budget: two boxes sharing a face or crossing stop within a few steps, and on theboundary_distancebenchmark a wall standing on a slab under a general placement measures in about 35 us instead of about 0.3 s (where the old search also ran out of budget at[0, 1e-3]), two crossing walls in about 55 us instead of 27 ms. A gap wider than the rounding margin is never taken for a touch, and boundaries that are apart measure bit for bit as before. Curved faces and circle or ellipse edges show no touch this way and are refined as before.
0.3.9 - 2026-10-03
Added
- Plan distance, clearance and overlap between bodies of several exact solids (#237):
body_plan_boundary_distance,body_plan_boundary_clearanceandbody_plan_overlaptake aPlacedBody(or a slice of items) per side, as thebody_*queries in space. The plan distance is the least over item pairs of the distance between their shadows on the XY plane, by one shared search, so far pairs are never refined; it is zero where an item's shadow overlaps or lies inside one of the other body's, and the witnesses name their items (BodyDistance).body_plan_overlapanswersBodyPlanOverlap: an overlap of positive area with the two items that show it, a certified gap, or undecided. Items of one body may overlap or touch freely in plan, so no layout is checked. The placement is applied before projecting along the world'sz: a body turned aboutzmeasures as its plan turned, and a tilted body casts its tilted items' shadows.
Fixed
plan_overlapno longer leaves undecided an overlap that two level planar faces show when their shadows overlap only in part (a block over part of a column's disc): when the distance search shows neither an overlap nor a gap -- its budget taken by walls and edges whose shadows merely cross, or its run ended by a plan distance of zero met between two boundary points -- a second search over patches of planar faces that are not vertical alone, coarsest pair first, looks for one.
0.3.8 - 2026-10-03
Added
- Boundary distance and Hausdorff distance between bodies of several exact solids (#229), one
ExactBRepper item in a slice:body_boundary_distanceandbody_boundary_clearance(the least distance over item pairs, by one shared branch and bound, so far pairs are never refined),body_boundary_hausdorff_distance,one_sided_body_boundary_hausdorffand its_with_budgetform (the Hausdorff distance between the boundaries of the unions). Witnesses name their items (BodyDistance,BodyHausdorffBounds,BodyHausdorff). The Hausdorff queries first show every pair of items of a body apart (a plane with a certified positive gap between them, or boundaries certainly apart with neither inside the other), touching without a shared patch of face (a plane between them within rounding where one of them has no face), or in exact face contact. Items in face contact (a column on its footing, blocks sharing a wall, stacked steps) share a patch that is interior to their union: each face in contact is cut down to its free region by an exact arrangement of the faces' boundaries on their common plane (axiolid-overlay'sArcArrangement, now an optional dependency underexact), and the union's boundary is measured. Contact is exact when both faces lie, in the B-rep's own numbers, on one plane normal to a coordinate axis, which a placement turning about that axis keeps. A gap is never glued: two items a sub-millimetre apart are measured as apart, both facing faces on the boundary, whatever the tolerance (gluing and widening the interval by the gap would not be sound: a lifted column's base disc is a whole radius from the glued boundary). Anything else is refused by name in the newBodyMeasureError:ContactPlaneNotAxisNormal(items touching on a plane no coordinate axis is normal to, such as walls turned in plan),ItemsNearlyShareFacewith the gap (items interpenetrating by no more than the caller's tolerance, or faces on axis planes at coordinates differing below rounding),ItemsShareFace(a face in contact that cannot be cut: a B-spline face, an elliptical edge) andItemsOverlap. Everybody_*query takes each side as aPlacedBody: the items in the body's own frame and one rigid placement (a slice, vector or array of items converts with the identity). Contact is found and cut in the body's frame, where an IFC body's extruded items stand on axis-normal planes, and the placement then moves the cut boundary, so a body turned in any direction keeps its contact cut; witnesses are in the world. Items turned against each other within the body's frame are still refused asContactPlaneNotAxisNormal, and a placement that is not rigid asPlacement. Distance needs no such check. A translated multi-item body closes as fast as one solid: faces match across items, a free region is bounded through the face it was cut from (so a cut that falls out differently in the last bit for the moved copy still closes at once), and each item's own support point seeds the lower bound. Ten stacked steps turned in plan and moved close both ways at 1e-9 in about 0.14 s.
0.3.7 - 2026-10-02
Added
one_sided_boundary_hausdorff_with_budget(#227): the one-sided boundary Hausdorff distance with the caller's cap on the splits, so a consumer bounds the work per pair. The interval is sound at any budget, only wider when it runs out.
Fixed
boundary_hausdorff_distancetook its matched fast path only for faces with identical pcurves (#227). Prisms fromboolean_arc_prisms_exactare trimmed in world coordinates, so two of them a translation apart were treated as unmatched and closed at first order: [1.0, 1.59] mm at accuracy 1e-4 after the whole 200k-split budget. Faces are now also matched as translates, independently of how the B-rep was built: same family, axes and shape (radius, semi-axes, angle, a cone's apex), and trims equal after the parameter shift the translation induces (for a plane the shift across its axes, for a cylinder, elliptical cylinder or cone along its axis only), to within a gate of 1e-9 relative. The other face is re-charted by the shift and bounded as before, the measured trim residue folded in, so the bound is|t|; a turned or resized face, by however little, is never matched this way. The lower bound is seeded with the support point against each matched displacement, exactly|t|from a translate's boundary. Those prisms, square, round or with an arc in the section, moved by 1 mm or 0.2 m in any direction, now close to 1e-6 without a split.
0.3.6 - 2026-10-02
Added
- Certified Hausdorff distance between exact boundaries (#224), behind the
exactfeature.boundary_hausdorff_distancereturnsBoundaryHausdorff: intervals certain to contain the two-sided Hausdorff distance between the boundaries of twoExactBReps and both one-sided ones, refined to a requested absolute accuracy;one_sided_boundary_hausdorffmeasures one direction. Each interval is aHausdorffBounds, as for meshes, with witnesses: a point certainly on the measured boundary realising the lower bound, and its nearest point found on the other. Lower bounds are such points' distances bounded below by the certified distance search; upper bounds come from branch and bound over face patches, each bounded by a matched face of the other boundary (same family, same trim:|S_A - S_B|over the patch, exact for a translation) or by the 1-Lipschitz distance about a point. Copies offset by a translation and identical copies close in a few dozen splits at any accuracy (two columns 0.1 mm apart to 1e-6, an arched opening moved 0.2 m, a re-export within the accuracy); other pairs close at first order in the patch size. A fixed budget can end refinement early; the interval stays sound, only wider.
0.3.5 - 2026-09-30
Added
- Certified mesh Hausdorff distance (#148).
hausdorff_distancereturnsMeshHausdorff: intervals certain to contain the two-sided Hausdorff distance between two triangle-mesh surfaces and both one-sided ones, refined to a requested absolute accuracy;one_sided_hausdorffmeasures one direction. EachHausdorffBoundscarries witnesses: the sample realising the lower bound and its nearest point on the other mesh. Lower bounds are sampled points' distances bounded below through each triangle's support function; upper bounds come from branch and bound over subdivided triangles (the distance to a triangle is convex, so a piece's maximum is at a corner) with a best-first BVH, and flat convex patches of the target (edge pairs, closed fans) bound pieces across seams with a certified hull excess. Rounding is accounted for with explicit margins, including the drift of rounded subdivision midpoints. Open, non-manifold and degenerate meshes are measured; empty meshes, bad indices, non-finite positions and invalid accuracies are refused withHausdorffError. - Hausdorff distance between polylines (#147), one-sided and two-sided, in 2D and 3D:
polyline_hausdorff_distance,one_sided_polyline_hausdorff_distanceand their_2dforms. The supremum over a segment of the distance to the other polyline is found among its ends and the points where two features of the other polyline -- vertices, segment interiors -- are equally near, since the distance to one feature is convex along a line. frechet_decide_certifiedandfrechet_decide_certified_2danswer whether the Fréchet distance is at mostepsonly when rounding cannot change the answer:FrechetDecision::AtMost,MoreThan, orUndecidedwithin the error margin of the floating-point decision.
0.3.4 - 2026-09-30
Added
- Plan relations between exact bodies (#217).
plan_boundary_distanceandplan_boundary_clearancecertify the distance between the bodies' projections onto the XY plane, with the contract ofboundary_distance: the interval contains the distance, and the witnesses lie on the boundaries with their projectionsupperapart. A shadow is its boundary's, so the same search runs with horizontal gaps, directions and enclosing discs; the result is zero where the shadows overlap, including a body standing inside another's footprint.plan_overlapreturnsPlanOverlap::Overlappingwith a plan point inside both shadows when two planar faces, not vertical, share an open patch in plan (a column on a slab),Disjointwith a certified gap, orUndecided.
0.3.3 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.2 - 2026-09-27
Added
- Fréchet distance between polylines (#147):
frechet_distance(the continuous distance: the least critical value of the free space that the Alt-Godau decision accepts),discrete_frechet_distance(Eiter-Mannila,O(nm)time,O(m)memory) and the decisionfrechet_at_most, each with a_2dform. Empty polylines, non-finite points and an invalid leash areFrechetErrors. Floating point, not certified.
0.3.1 - 2026-09-27
Added
Face domains trimmed by pair sections of two B-splines: the section's nodes are kept as breaks, where its parameter speed changes.
Face domains for B-spline and lifted pcurves. Turning points come from a dense scan of the derivative's signs with bisection, and a B-spline's knots are kept as breaks.
Turning points of
Curve2::Implicitpcurves, isolated with interval bounds, so faces trimmed by them get a certified domain.
Fixed
FaceDomain::containscounts winding -1 as inside (a face whose loops run clockwise in its parameters). It also moves a point by whole periods of a torus's second angle into the face's box.Face integrals split an implicit pcurve at its cell boundaries, where its parameter's speed jumps, so adaptive quadrature converges there.
FaceDomain::containsdecides a point whose ray passes through a boundary vertex or along a boundary piece, by simulation of simplicity (the ray runs just right of the point), instead of answeringNone. The centre of a diamond, whose every axis ray meets a corner, used to be undecidable.
Added
FaceDomain(#167): an exact face's parameter domain, answering whether a point lies in the face with a certificate or not at all; a point outside the domain's box in a coordinate that does not wrap is decided at once.boundary_distanceandboundary_clearance(#125, ADR 0074): an interval certain to contain the distance between the boundaries of two exact B-reps, with witness points on both, and a comparison with a limit that answersClearance::Below,AboveorIndeterminate. Branch and bound over face patches and edge spans; lower bounds from Lipschitz spheres and exact projection ranges, upper bounds only from points certified on the boundary.exact_propertiesmeasures curved faces (#125, ADR 0073): cylinders, cones, spheres, tori, elliptical cylinders, B-spline faces, and planar faces bounded by arcs or ellipses. Each face is integrated over its own parameter domain by Green's theorem round its pcurves, with adaptive Gauss-Kronrod quadrature held to a relative error of 1e-13; nothing is tessellated. Seams, poles and apexes, and torus faces bounded by meridians are handled; a boundary that encloses nothing in the surface's parameters is refused.
Changed
ExactMeasureErroris unchanged in shape (exhaustive, four variants), so this is a patch release. A face the module cannot integrate -- a surface family it has no integral for, pcurves that bound no domain, a surface it cannot evaluate there, an integral short of its error bound -- isNonPlanarFace, whose name predates curved faces, with the reason. The display text reads "cannot integrate a face (reason)".- The
exactfeature now also enablesaxiolid-evaluateandaxiolid-curve.
Fixed
FaceDomainand the certified distance read a point on a periodic face with a negative angle as outside it (#167): the whole-period shifts tried had the wrong sign, so-0.2was tried at-0.2 - 2 pi, not at2 pi - 0.2. Faces reaching a pole hid it, since the pole adds its own crossing.A planar face with a hole reported the hole's area added to its own: the fan summed triangle magnitudes. Areas are now summed as vectors, so a hole subtracts (a 4 x 4 plate with a 2 x 2 hole read 20 per cap, not 12).
exact_propertieshonours face, shell-use and bound orientation. It read loop winding alone, which is only right for faces used forward; aReversedcap off the planez = 0added its volume instead of subtracting it (a unit cube at2 <= z <= 3measured 7/3). Every solid tested before sat onz = 0, where the error vanishes.
axiolid-mesh
0.3.6 - 2026-11-04
Added
audit_mesh_cycleandtry_audit_mesh_cyclereturnMeshCycleHealth: theMeshHealthplus the number of unbalanced edges and the first one, so a mesh closed up to edge-touching shells can be told from an open one. A closed oriented cycle is weaker than a closed two-manifold (#416).The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).split_triangle_at_side_cornersre-triangulates a triangle whose sides carry extra corners into proper triangles of the same winding, by side bitmask ear clipping on indices alone.axiolid-mesh-compileandaxiolid-healshared it as two private copies before (#422).
0.3.5 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.4 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.3 - 2026-10-10
Added
- An optional
serdefeature, off by default, derivingSerializeandDeserializeforTriMesh,PolygonMeshand the values they hold: the values the geometry graph's versioned wire format carries (#267, ADR 0085). Enums are externally tagged by their variant names and unknown fields are refused. No default-build change.
0.3.2 - 2026-10-02
Added
HalfedgeMesh(#140): an editable halfedge surface mesh with O(1) adjacency, laid out like CGAL'sSurface_mesh(edgeeowns halfedges2eand2e + 1, sooppositestores nothing). Built from aTriMesh(from_tri_mesh) or polygonal faces (from_faces) keeping the input numbering, converted back withto_tri_mesh, which reproduces the input index buffer exactly.- Navigation:
next,prev,opposite,source,target,face,edge,vertex_halfedge,face_halfedge,find_halfedge; counter-clockwise circulatorsoutgoing_halfedges,incoming_halfedges,vertex_vertices,vertex_faces; face and hole loopsface_halfedges,face_vertices,face_faces,loop_halfedges,boundary_halfedges,boundary_loops. A boundary vertex stores, and circulates from, its boundary halfedge. - Local edits that refuse by name and keep every invariant and the Euler characteristic:
flip_edge,split_edge(re-triangulating adjacent triangles),collapse_edge(link condition, with tetrahedron, lone-triangle and pillow guards),split_face(centre fan),split_face_diagonal; plusfill_holeandcompact, which renumbers densely and returns aHalfedgeRemap. HalfedgeMesh::validatechecks every structural invariant and reports aHalfedgeInvariantError.HalfedgeBuildErrornames the input a halfedge mesh refuses:NonManifoldEdge,NonManifoldVertex(a bowtie or two closed fans at a point),InconsistentOrientation,DegenerateFace,FaceTooSmall,IndexOutOfRange,IncompleteTriangle.HalfedgeEditErrornames refused edits (LinkCondition,WouldDegenerate,EdgeExists,BoundaryEdge,NotATriangle, ...).- Typed ids
VertexId,HalfedgeId,EdgeId,FaceId.
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.0 - 2026-09-23
Added
AttributeFate::then: the fate of a channel through two sequential steps (dropped wins and keeps the first reason; any interpolation interpolates).- Corner-indexed attribute channels (#112):
AttributeChannel::corner_indices, one entry per triangle corner, mirroringNormalAttribute::indices. Source formats store texture coordinates this way; positions stay shared, so UV seams no longer force a choice between splitting vertices (breaking closure) and smearing values. AttributeChannel::corner_indexed,is_corner_indexed,value_count,at_corner(reads either addressing,Nonefor an unmapped corner), andAttributeChannel::UNMAPPEDfor triangles that carry no value.validate_structurechecks corner channels: whole tuples, one entry per corner, entries in range, and each triangle fully mapped or fully unmapped. NewMeshValidationErrorvariants name the channel.DropReason::ConflictingValues: merged vertices carried different values, so a per-vertex channel could not keep both (#114).DropReason::IncompatibleChannels: inputs being combined define one channel name with a different width or blend (#115).
Changed
- Breaking (minor slot pre-1.0, ADR 0067):
AttributeChannelgains the public fieldcorner_indices, so struct-literal construction must addcorner_indices: None;AttributeChannel::newis unaffected.DropReasonis now#[non_exhaustive], so an exhaustivematchon it needs a wildcard arm. No caller in this workspace or in openbim does either.
axiolid-mesh-boolean-boolmesh
0.3.7 - 2026-11-07
Added
- The package now ships
LICENSE(a symlink to the repository root), so the licence text travels with the archive (#358).
Changed
- The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.6 - 2026-10-14
Fixed
- A memory budget holds for a boolean whose result is much larger than its operands (#308). The declared bound counts input triangles only, and two crossing combs of 32 teeth (1,048 triangles) peaked at 8.8x it in their union and 31x in their symmetric difference. The declaration stays the admission bound on the operands; with a budget, the solve now counts the result's half-edges exactly once the intersection pass has found the crossings and refuses with
BudgetExceededbefore allocating the result when the declaration plus 448 bytes per half-edge beyond three per input triangle does not fit. The intersection pass stops as soon as its crossings alone could not fit. Results within three half-edges per input triangle, which ordinary booleans are, cost nothing beyond the declaration. - A composed
SymmetricDifferencecharges each step the union and intersection it holds, instead of passing every step the whole budget. union_manyandsubtract_manygive each boolean the budget the batch does not hold, split evenly between the pairs of aparallel-batchlevel, so booleans running at once cannot together charge their results against the same bytes.
Changed
- A budget that admits the declared bound can now be refused inside the solve, for a single boolean as already for a batch: the check before dispatch is necessary, not sufficient.
scratch_probemeasures crossing combs, slab soups and pierced slabs (scratch_probe combswith no budget,scratch_probe crossingagainst budgets), andtests/scratch_bound.rschecks them.
0.3.5 - 2026-10-13
Fixed
- A memory budget holds for
union_manyandsubtract_many(#259). The declared bound covers one boolean in its operands' triangles; a batch has no such bound in its input, because union intermediates outgrow their operands and a cut subject grows with every group, and withparallel-batcha worker waiting inside one boolean's join steals other pairs, so every pair of a level can be in flight on fewer workers (5 on 4 workers was observed). Each step is now charged when its operands exist: the declared bound for every boolean that can be live (all pairs of aparallel-batchlevel at once, one otherwise, the per-worker term once), plus what the batch holds (its copies, the previous level's results, the cut subject, a fused tool, its node and index lists). The first step that does not fit is refused withBudgetExceededbefore it starts. Before,union_manywas charged once for its input triangles andsubtract_manynot at all beyond each boolean's own check, which only ran withparallel. - A boolean given a budget directly, without dispatch, is checked in every build, not only with
parallel.
Changed
union_manyborrows the solids that need no channel conformance and moves an odd trailing solid to the next level instead of copying it, andsubtract_manyborrows the subject until the first group cuts it: each copy was scratch held through the batch.- The per-worker term no longer includes the 64 KiB base with
parallel-batch. That was the batch's concurrency charged per worker, which stolen work can exceed; the batch now charges it per pair. Withparallel-batchalone the term is the 16 KiB of a worker's bookkeeping. - The
scratch_boundtest and thescratch_probebinary measure both batch paths at 14 workloads that grow (overlapping boxes, a slab cut by overlapping tools, at several sizes, with and without an attribute channel) on 1 to 64 workers under the forced schedule: each run at the smallest budget it admits stays within it (at most 73% of it), the run a byte below is refused within it, and a 1 KiB budget is refused before any copy is made. A window now counts only its own allocations: memory of an earlier pool freed inside it (rayon retires deque buffers through an epoch collector) hid up to 7 KB of a peak. tests/batch_budget.rspins what each step is charged, andscripts/probe_scratch_bound_mutants.pykills a charge that drops any of its terms.
0.3.4 - 2026-10-04
Fixed
- The declared scratch is an upper bound on a pool as well (#226). The old
PerElement { bytes_per_element: 4096 }covered large inputs but not small ones on rayon: each worker allocates its own bookkeeping when it starts, and on a loaded machine that lands inside whichever boolean is running. 24 triangles peaked at 113,608 bytes against the declared 98,304 in one gate run. The provider now declaresScratchRequirement::Affinewith a 64 KiB base, 1,536 bytes per input triangle of all operands, and 16 KiB per worker withparallel(plus the base per worker withparallel-batch, where each worker can run a boolean of its own; zero without either feature). Each term is 1.7x to 2.9x its measurement; the doc comment ofscratch_requirementhas the table. - With a memory budget, a boolean (and a
parallel-batchunion_many) re-checks that bound against the width of the rayon pool it runs in and refuses withBudgetExceededwhen the pool is wider than the budget check before dispatch assumed.
Changed
tests/scratch_bound.rsand thescratch_probebinary measure under a forced worst-case schedule instead of whatever the machine's load gives: with rayon each boolean runs in a pool of 1, 2, 4, 16 or 64 workers started inside the measured window, with allocations stalled so join halves are stolen. The test checks each peak against the bound for its worker count, and checks that worker start-up is really inside the window. Under the old declaration it now fails every run instead of occasionally under load.scripts/probe_scratch_bound_mutants.pykills a dropped base term, a dropped per-worker term, a worker count ignored in the contract, the dispatcher or the provider, a boolean budgeted for its subject alone, and the old declaration.
0.3.3 - 2026-09-30
Fixed
- Unions of overlapping axis-aligned boxes no longer refuse inside the solve with an odd edge-point count (#203). The winding number's xy broad phase rejected queries beyond
min + cell * dim, a rounded product that can fall one ulp short of the operand's true bounding box, so a vertex lying exactly on the other operand's extreme plane lost a face from its winding number. The grid now rejects against the exact bounding box. Grid unions at pitches 0.6 to 0.8 with k = 5 and 6 complete, sequentially, throughunion_manyand on the fast winding path, with the exact volume and a closed, consistently wound result (tests/overlapping_grid.rs).
Changed
tests/solve_failure.rsis replaced: no admissible input is known to reach a refusal inside the solve now, so the mapping toBackendContractViolationis pinned by a unit test wherecompute_boolean's error lands.
0.3.2 - 2026-09-28
Changed
- Behaviour change: a refusal inside the solve (an odd edge-point count in
pair_up, #101) is reported asGeomError::BackendContractViolationnamingboolmesh, notDegenerate. The operands passed every input gate, so the failure is this provider's defect and no longer reads as the caller's.tests/solve_failure.rspins it on a grid union that still reaches the refusal (#203). - The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-27
Fixed
- The scratch probe discards one warmup boolean before measuring, and measures peaks above the bytes already live, so the first operation is no longer charged for process startup (#110). A
scratch_boundtest fails if any measured peak exceeds the declared 4 KiB per triangle.
0.3.0 - 2026-09-23
Added
- The pairwise boolean carries attribute channels (#116). Each result triangle's source triangle is tracked through the CSG core, so a corner that is a source corner copies its value and a corner on a cut is derived in its source triangle under the channel's
Blend. Output is corner-indexed; faces from a tool without the channel areUNMAPPED. Fates:Preservedwhen nothing was derived,Interpolatedotherwise,Dropped(NotBlendable)for aBlend::Nonechannel a cut needs. Every path carries channels (#116, completed): the analytic box path (sources recovered by plane lookup), groupedsubtract_many(fused cutters keep theirs), treeunion_many(each solid conformed to the first's channel set) and the empty result.
Fixed
- A result corner was sampled in its RECORDED source triangle, but simplification merges coplanar faces, so a result face can span several source triangles: corners were extrapolated (weights down to -0.65), wrong for piecewise data such as atlas UVs or per-face ids. Each corner is now located in the coplanar source region by a point just inside its face, which also picks the right side of a seam.
dedupe_edgepushed per-face data indexed by a vertex id when pinching a vertex, desynchronising face normals and provenance from the faces (inherited from upstream; Manifold pushes only per-vertex data there). TheProviderLimitationcomment claiming the boolean returns positions only, stale since ADR 0047, is corrected.
axiolid-mesh-boolean-contract
0.3.5 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.4 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.3 - 2026-10-13
Changed
MeshBoolean::scratch_requirementsays what it means for the batch methods (#259): the bound of each boolean on that step's operands, not of the batch in its input triangles. A provider whose batch holds intermediates or runs booleans concurrently charges each step when its operands exist and refuses withBudgetExceededbefore the first that does not fit, so the registry's check before a batch is necessary, not sufficient.
0.3.2 - 2026-10-04
Changed
MeshBoolean::scratch_requirementsays what it is charged for: the input triangles of every operand together, and the worker threads the call runs on (#226).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.0 - 2026-09-23
Added
merge_fates: compose per-channel fates across sequential steps (#116).
Fixed
- Composed evidence reported only the last step's attribute fates:
BooleanEvidence::absorb(thesubtract_many/union_manydefaults) andsymmetric_difference_via_compositionnow compose them, so a channel a middle step derived or dropped is reported that way.
axiolid-mesh-compile
0.3.46 - 2026-11-14
Changed
- Flattening a 2D chain is narrowed (
axiolid_reference::bound, #438): a chain with a sinusoid or two-point polyline parametric piece is no longer claimed. Curve-bounded boundaries and profile segments over such chains go from proven to unbounded, and a compiled polyline of such a chain is refused (UnsupportedInput, "an arc-length chain with a piece no certified position table reads"). An elevated curve over one was already unclaimed in 0.3.45 (#393). A claimed family whose chord bound is missing on a span is refined and, at the depth limit, refused, never accepted on the midpoint's sagitta; a chain with a fast intrinsic piece, whose evaluated ends are centimetres off, can reachBudgetExceededbelow a tolerance its ends' gap exceeds. - A swept disk along an elevated or banked curve over a chain plan is no longer unbounded by name (#438; 0.3.45 reported
UNCERTIFIED_DIRECTRIX_POINTSfor it, #393): see Fixed. Under an intrinsic profile it stays unbounded (TODO(#481)).
Fixed
- A swept disk along an elevated curve over a chain plan (a line, a cubic transition and an arc) is certified against the exact tube (#438): the centre line, its tangent and its second derivative are read from the
ElevatedPositionTableover the chain, and the bound charges their errors (wall_jet_charge, held against adversarially perturbed jets in a unit test). The evaluator's points were taken as exact there: the arc-length inverse's tolerance and every placement before a piece carry into them. A 0.3 m disk along such a plan over[5, 95]at 1 mm is certified at 1 mm and the exact tube lies within the reported bound. A swept disk along a chain with a sinusoid or two-point polyline piece was certified in 0.3.44 from the evaluator's points; it is now unbounded, by the generic reason ("the directrix could not be bounded"), since no certified arc length exists for such a piece.
0.3.45 - 2026-11-13
Added
SKIN_WITHIN_ROUNDING: the start of theDegeneratemessage a difference or an intersection is refused with when its result holds a skin thinner than its coordinates' rounding and the exact referee cannot decide it (#400, ADR 0102).B-rep faces on a
SurfaceRelation::Revolution,LinearExtrusionorRuledmesh when every edge use carries a pcurve (#411, ADR 0103), under the familiessurface of revolution,surface of linear extrusionandruled surface, with a certified deviation. The tessellator reads a face's surface through a crate-private support view, and the three relations evaluate and bound their derivatives in closed form from the swept curve or the two sides, a line, circle, ellipse or B-spline (rational too). A B-spline is read only inside its domain and a ruled surface inside its unit square. Exact compilation of such a face stays refused. A boolean with such a B-rep operand now meshes, reported unbounded by name (exact boolean over a B-rep operand with a face that is neither a plane with straight edges nor a warped face without a surface); it was refused at tessellation before.deviation::MESH_TO_EXACT: the detail of the other direction of a measured boolean, how far any point of the mesh lies from the exact compiler's result (#329).Stationed runs, placements and section folds along a banked curve over an intrinsic plan (a canted alignment, #396) are certified where they were unbounded by name: the frame law bounds the roll from the cant law and compares every frame read with the exact rolled section. A fold inside a canted transition is found between the stations and refused ("sections fold"); it was checked conservatively and never refused. A canted run over a chain plan stays unbounded by name (
TODO(#394)).A certified bound for the cells beside a mitre of a run of sections at stations, on a curved piece, with a changing section or a seam inside the cell (#335, ADR 0082). The exact solid at a mitre is each side extruded along its end tangent to the mitre plane (moved by the longitudinal offset) plus the planar faces between the two sides' cuts; the bound is closed form per cell and the mesh is unchanged. The openbimrs/ifc#443 deck, unbounded by name in 0.3.44, reports
Certified(4.88e-4), within its 1e-3 request.deviation::EMPTY_RESULT: the detail of a boolean whose exact result is empty and whose mesh is too, certified at0(#326).
Changed
A swept disk along an elevated or banked curve over a chain plan, or under an intrinsic (vertical clothoid) profile, is reported unbounded by name (
UNCERTIFIED_DIRECTRIX_POINTS, "the directrix's points have no certified error") where it was reported certified (#393). The certificate read the evaluator's points, which have no certified error.TODO(#438)lifts it for chain plans,TODO(#481)for intrinsic profiles.An elevated curve over a chain plan is no longer claimed certified for flattening (
axiolid_reference::bound::certifies_flattening3, #393,TODO(#438)): its chord bound under-reported the evaluated ends, so its polylines and curve-bounded boundaries are reported unbounded by name where they were reported proven.A flush slab whose openings were round-tripped through world coordinates (their sides moved by
9.3e-10) is refused by name,SKIN_WITHIN_ROUNDINGwith the referee's reason, where 0.3.44 returned it with every opening lidded; the exact compiler cannot classify below that feature (#457). The same slab placed, or with exactly turned openings, now opens every opening where 0.3.44 returned a self-intersecting mesh (#400, ADR 0102).A B-rep face on a surface relation other than a revolution, a linear extrusion or a ruled surface is refused as
a B-rep face on a surface relation other than a revolution, a linear extrusion or a ruled surface(it wasa B-rep face on a surface relation).An edge use without a pcurve on a face over one of those relations is refused as
an edge use without a pcurve on a face over a surface relationbefore any meshing, and a swept curve that is not a line, circle, ellipse or B-spline is refused asa swept curve other than a line, circle, ellipse or B-spline(a swept curve that is not an atomic 3D curvefor a node of another kind); a zero axis or direction is invalid input (#411).An exact boolean whose result has a vertex off its edge's curve beyond what its report allows is refused by name (#445). It needs
axiolid-brep-booleanwithBooleanError::VertexOffCurve.An intersection of half-spaces (read through instances) is the tool of a difference or an intersection in both compilers, and so is a single half-space on the left of an intersection with a solid, which commutes (#326). Each further half-space's stand-in is sized by the stand-in so far, so a region of one is the single clip it was, bit for bit. The mesh stand-in of two or more passes the snap perturbations of cutting them together (
SNAPPED_OPERANDS) into the result's deviation. A region is cut in its subject's frame too (ADR 0102), each leaf relative to it.A half-space on its own, or in a boolean that is no clip, is refused by name in the mesh compiler as
UnsupportedInput(half-space without a subject to size it by: ...,boolean of a half-space that is not a clip: ...), where it wasUnsupportedwith no input. The exact compiler names the new cases:exact intersection of half-spaces without a subject: ...andexact symmetric difference with a half-space, which is unbounded. A symmetric difference's refusal now comes after the empty operands' identities, so an operand's own refusal comes first.A cell beside a mitre that 0.3.44 reported unbounded ("a cell beside a mitred seam on a curved piece, or with a section that changes across it") is bounded, or named by one of two reasons refinement splits: "a cell beside a mitre does not advance across the mitre plane fast enough to bound the plane's cut" (a corner too sharp for the piece's turning over the cell) and "a cell between two mitres, on a curved piece or with a changing section, is too short to split". Runs that were unbounded there are refined near their mitres and report a bound (#335).
Fixed
A swept disk along an elevated or banked curve over an intrinsic plan (a clothoid) is certified against the exact tube (#393): the centre line is read from an
ElevatedPositionTablebuilt at a thousandth of the budget, whose error the bound carries, where the evaluator's own point was taken as exact. Only a plan under a fast curvature law is far off: a 0.3 m disk along a plan of curvature0.002 sin(50 s)over[95, 98]at 1 mm was reported certified at 1.20e-3, and the exact tube lay 2.84e-3 from its mesh. On a clothoid the difference is about1e-12, within the table's error.A measured boolean whose exact result is all planar reports the other direction too (#329), as its own
DeviationPath::Booleancontribution underMESH_TO_EXACT, widened like the boolean's own bound by the flattening, the readings and the rounding floor: the mesh's triangles searched against a welded, outward-oriented triangulation of the exact faces. Material the mesh has and the exact result lacks, connected to the body, now shows: an uncut full-width clip reports 4.76 where it reported 0.175, and the strip of #325 at most 1.2 times 238.549 mm where it reported 175 mm. Consumers that read "the" boolean contribution now see one more. A skin authored within the tolerance that the exact compiler read away shows too, and may failmeets_requested. Curved results keep the one-sided bound (#348). The coordinates are taken relative to the result's own extent, so a wall far from the origin is bounded as tightly as one at it. The report of the two-door wall takes 1.4 to 1.6 times as long in the release bench (492 ms on main; 644 ms and 783 ms in two runs with the change), and a three-opening wall four million metres from the origin takes 6.8 s in a debug test, forward and reverse.A half-space clip is cut in its subject's frame, so a wall turned by a placement no longer comes back as a
Solidwith a lid thinner than rounding across a slot the clip opens (#400, ADR 0102). A difference or intersection whose result still holds such a skin is decided by the exact referee, bounded half-space tools included, where it reads nothing above the rounding reach, and refused by name otherwise.A sectioned surface and an open-section run (
SurfaceRelation::SectionedSurface,OpenSectionsAtStations) whose section planes stop advancing are refused as invalid input, as a stationed spine is since #361 (#407). The same decision applies: exact for unturned sections on closed-form bend frames, a touch within rounding included (such a sheet was refused as a collapsed triangle before, now as a fold); conservative for turned sections and frames read by quadrature, never refusing and reporting the sheet unbounded by name when a fold cannot be excluded. A sheet that meets itself far away is not detected yet. The refusal reads 'station-placed sections fold' for spines too.A curved face's pcurve may walk a shared edge at another pace than the pcurve the edge was sampled through (#413). Each cached sample is located on the use's own pcurve: the parameter whose surface image is nearest the sample, searched with Brent's method between the sample before and a guess scaled by the pcurve's pace, so the parameters stay strictly monotone. A sample with no place on the pcurve, or further from it than the chord budget, is refused (
a shared edge sample is not located on a neighbouring pcurve), where the pcurves were compared at uniform parameters before and a rim whose wall and cap circle differ in pace (a traced section of an exact union, an IFC pcurve) was refused as disagreeing in 3D. A located parameter within a thousandth of a step of its uniform parameter is put there, so a pcurve at the sampling use's pace triangulates as before. The deviation bound takes the chord bounds over the located spans, across a break piecewise (each piece's bound plus the break's distance from the span's chord, where a span with a break inside had none), and the samples' offsets from the surface once, in the triangles. The edge pass halves its spans until the located spans of every other use meet the budget, up to the segment cap. An exact result closes a pcurve's end onto its vertex within the smaller of its boolean's reading and the tolerance (and never less than the chord budget); the disagreement is part of the bound.A boolean with a face reaching a cone's apex or a sphere's pole is measured (#379). The deviation oracle no longer refuses the pole gap of such a face's loop ("an exact face's loops do not close in its parameters"): a loop that leaves the pole at the height it reached it closes there, whatever the jump in u. A cone's box ends at its apex, where its derivative bound is undefined, so the face is no longer reported unbounded ("cannot be bounded over its domain"). A partial cone keeping its apex, cut from a box, is certified within the budget.
The B-rep tessellator refuses a curved face whose trim closes across a sphere pole or a cone apex by name, as
UnsupportedInput("a curved B-rep face whose trim closes across a sphere pole or a cone apex"). It used to exhaust the refinement depth after seconds (BudgetExceeded) or fail with "ring folds back"; the pole row of samples isTODO(#463).The certified bound of a straight cell beside a mitre no longer assumes the mesh's mitre row lies on each side's own cut (#335). It does not where the two sides' cuts differ: a turn on a grade in the section frame, which 0.3.44 reported
Certified(0.0987)while the exact walls stray 0.1007 (now 0.2236), and a longitudinal offset at the mitre, which it reported at 3.6e-11 while the exact walls stray 0.5 (now 0.5). Both may stop meeting the requested chord error.A boolean with an operand that leaves nothing (a clip that misses its subject, an intersection of disjoint solids) is decided by an identity in both compilers (
S - 0 = S,0 - T = 0,S & 0 = 0,S | 0 = S; #326). It was refused astool: mesh has no trianglesorsubject: mesh has no triangles(mesh), or asDegenerate(exact). The exact compiler still refuses an empty root,Degenerate, since anExactBRepcannot be empty; the mesh compiler returns an empty mesh,MeshClosure::Solid, as before. A skin a subject has pending (#400) passes through an identity to the root of its chain, where the referee decides it. An emptiness the general boolean reads only within the tolerance (operands overlapping by less than it) is not an identity: it is refused asDegenerate, as before, since the other operand would pass through with an exact report and the cavity dropped. The stand-ins a region builds are the exception: a meeting read within the tolerance there can be a touch at their margin, so the result at zero tolerance is taken, and a tool whose box lies wholly apart from the subject's envelope leaves nothing of the region there, decided exactly before the cut. A boolean whose exact result and mesh are both empty reportsEMPTY_RESULTat0, widened by the report's readings and rounding floor, and unbounded when the empty result is that of flattened operands; an empty exact result against a mesh with triangles, or the reverse, is unbounded by name.A region, or a union with one, of tens of thousands of nested intersections no longer overflows the stack: whether a node is a region is decided on demand, iteratively, and remembered for the compilation or the batch, so a chain of n clips costs O(n) in all and a batch of roots does not pay the graph once per root (#326).
0.3.44 - 2026-11-03
Added
PolylineCompilercompiles a trim of a trim, of an offset, of a placed curve and of a composite (#424, ADR 0101). A trim reads its basis's parameter, an offset its basis's, a placed curve its source's and a composite its 3D arc length, as a swept directrix does: a window a trim above keeps narrows the inner trim's span, modulo the period on a closed conic. A trim of a composite is cut through the station flattening, each piece flattened between the native parameters a station reads, and the bound adds the certified error of a cut inside an ellipse or B-spline piece. Refused by name: a point or arc-length selector on a relation (#432), a trim across a closed composite's closing joint (#433), a trim of a composite measured in plan distance, holding an instanced or parameter curve, or ending inside an offset segment, and an offset of a trimmed composite cut inside a quadrature piece. Requiresaxiolid-evaluate0.3.20.station::placement_accuracy,PlacementAccuracyandPlacementBound(#405): how far a node placed at a station lies from its exact placement,Exacton a line,Certifiedwith the error of the placement's point and of each axis (PlacementBound::displacementfor a local point), orUnboundedby name. Requiresaxiolid-evaluate0.3.20.SurfaceRelation::Ruled(#402) compiles to an open sheet (MeshClosure::Surface) with a certified deviation, the newDeviationPath::RuledSurface. A side is a line, circle, ellipse or B-spline (rational too) over an oriented interval, or an apex point; the bound is the sides' curve term plus the rulings' twist term, derived inruled.rs. A side on another curve family, a conic over more than one turn, a B-spline interval outside its domain and sides that coincide are refused by name.A union whose operands are placed by linear parts that differ in a bit, and whose contact the mesh cut cannot settle, is decided by the exact compiler (#391, ADR 0098): the union node is compiled exactly and the exact result tessellated through a resolved view of its own catalogs and pcurve intervals. The trigger is geometry: faces of the two operands that nearly share a plane within the linear tolerance (
axiolid_inspect::near_coplanar), or a pinch (#194) orUNMERGED_UNION_CONTACTrefusal. The result is accepted only as a closed solid that passes both guards, and is reported under the new detailsdeviation::REFEREED_UNIONanddeviation::REFEREED_UNION_EXACT, withCOINCIDENT_OPERANDSwhere the exact compiler read a feature within the tolerance. Its operands' attribute channels are dropped by name,ProviderLimitation.A stationed spine whose sections are different curved profiles that pair segment for segment (lines, circular and elliptic arcs: a disk growing, a rounded rectangle whose corners grow, an ellipse becoming a disk) reports a
Certifieddeviation instead ofUnbounded(#361, ADR 0082): the profiles are flattened together bypaired_profile_rings, so the bound of the flattening holds for every interpolated section, and it is the profile's share and the fold margin. A polyline or B-spline segment or a different segment structure stays unbounded by name, and the exact compiler still refuses a curved section that changes. Requiresaxiolid-constructwithpaired_profile_rings.deviation::TOLERANCE_READINGSanddeviation::BOOLEAN_ROUNDING_FLOOR(#421): the details under which a boolean measured against its exact result names the furthest any kind of reading within tolerance moved a feature, and the report's rounding floor.deviation::REFEREED_DIFFERENCE,REFEREED_DIFFERENCE_EXACT,REFEREED_INTERSECTIONandREFEREED_INTERSECTION_EXACT(#423): the details of a difference or an intersection the exact compiler decided.
Changed
A station on a trim of a trim (or of a trim placed at a station) reads the outer trim's parameter selectors in the inner trim's basis's parameter, as a swept directrix and a polyline do (#424, the ADR 0082 amendment of 2026-11-06), no longer as a distance along the inner trim. A nested trim's outer selectors that were distances along the inner trim name the basis's parameters now, so the point of every station on one moves, through
station::resolve,station::curve_pathand every run on it. Selectors other than parameters on such a trim are refused by name (#432). A window may lie past the inner trim by the linear tolerance along the curve (the tolerance over a certified upper bound of the carrier's speed, in parameter units; for a station basis, which has no options, by1e-12 * max(1, |x|), the evaluator's rule for a distance); by more it isInvalidInput.A node placed at a station on a circle, a polyline, an elevated or banked curve over a line or a circle, an intrinsic plan alone or under a closed-form profile, or a composite of them reports its placement certified (#405), where it was unbounded on every basis but a line: the frame's error
Fgrows every contribution of the instance, as the placement's rounding does, and is named once as a certifiedStationPlacementcontribution. The source is re-meshed with room for the rounding andFwhen they would carry it over the budget. Frames read by quadrature, chains, a basis itself placed inexactly and a station beside a seam it is not read on stay unbounded, now with their reasons named; the exact compiler still refuses every basis but a line.The B-rep tessellator reads its supports through a resolved view, an edge's 3D curve, a use's pcurve with its parameter span and a face's surface, instead of looking each handle up in the graph (#412). A handle that does not resolve to a node of its kind (an edge curve, a pcurve or a face surface) is now refused up front by name with
GeomError::InvalidInput, whether or not any face reads the handle. A shelled planar face whose edge use carries a pcurve handle to a Curve3 node, or whose edge curve handle points at a Surface node, was meshed (or reported Unbounded) before and is refused now. A B-rep with broken topology is still reported as such first.A B-rep face whose support is a surface relation (a ruled, swept or revolved surface) is refused as
UnsupportedInputforTessellation(a B-rep face on a surface relation), no longer asInvalidInput; it was never a malformed graph (#402).A curve-bounded surface over a surface relation, and a sweep whose reference surface is one, are refused as
UnsupportedInputforSurfaceEvaluation(a curve-bounded surface over a surface relation,a surface relation as a reference surface), no longer asInvalidInput(#402).A union of differently composed turns that was refused by name (
UNMERGED_UNION_CONTACTor a pinch) now fuses where the exact compiler fuses it, and one that came back as a body with sub-rounding wedge cracks is replaced by the exact union's tessellation. Where the exact compiler refuses too, the refusal keeps its message and ends with "; the exact referee refused: {reason}"; an unsettled but valid mesh result is returned as before. Mesh operands are never refereed.An edge curve without a p-curve is sampled at its derivative breaks first (#342): a polyline's corners, a B-spline's full-multiplicity knots and a closed curve's seam are samples, with uniform power-of-two spans inside each piece between them, shared by the faces using the edge. A straight polyline piece is one span with a chord bound of zero, so a sharp polyline edge no longer exhausts the segment cap and fails with
BudgetExceededat a tight budget (a closed square compiles at 1e-4 and 1e-6 with its four corners and its vertex, and a zero bound). An edge some use runs through a p-curve is sampled whole, uniformly, as before.The exact referee decides a difference or an intersection whose mesh holds a void the exact result lacks, bounded by skins within rounding of each other (#423, ADR 0098): a flush door cutter placed with its wall no longer leaves the cut-out void. It fires only when faces of the operands nearly share a plane within the linear tolerance, the mesh has more than one component and more shells than the exact result, and the exact compiler read the operands' faces no further than their rounding. The mesh is then the tessellation of the exact result, accepted only as a closed solid, or empty, with no pinch and no two bodies within the tolerance. Otherwise the mesh result stands and a measured deviation names the extra shell as unbounded (#392). A skin or void thinner than the tolerance but above rounding stays as authored (ADR 0096, D7, #291), and a half-space tool is not refereed. Unions keep their trigger.
A measured boolean is
Unbounded("boolean: the mesh has more shells than the exact result") when its mesh has more connected components, with bit-equal positions taken as one vertex, than the exact result has shells (#392). The measured distance is one-sided and cannot see an extra shell; fewer components are no refusal.
Fixed
A trim of a full-turn trim of a closed conic kept the wrong arc when an outer selector landed on the seam:
trim(trim(circle, 0, 2 pi), pi, 2 pi)was the near half[0, pi], and a swept directrix'sparameter_rangeon a full-turn trim read the same way (#424). An end that lands on the start of a full turn is its end when it is the larger one asked. The window of a nested trim and the sweep range of a directrix on a trimmed conic or a trim of a trim are read to the linear tolerance along the curve (the tolerance over a certified upper bound of the carrier's speed:1/rof a circle,1/|d|of a line,1/max semi-axisof an ellipse, the evaluator's derivative bound of a B-spline,1of a composite's arc length), as the tolerance a length is accepted to, no longer to the linear tolerance taken as a parameter. Where no speed bound is known the window is read to1e-12 * max(1, |x|), failing closed.A station-placed spine whose section planes fold is refused as invalid input, by the mesh and the exact compiler alike (#361). A section vertex on the inner side of a bend at or past its centre of curvature (the plan arc's radius against the section's lateral extent, a vertical arc's against its up extent in the section frame), or one receding along the tangent faster than the basis advances, makes the sections cross: the loft was meshed into a self-intersecting shell and reported certified. The check is exact where the frame law has a closed-form bend (unturned sections along a line, circle, polyline, elevated or banked curve over those, or an intrinsic curve; a touch within rounding is refused too) and conservative elsewhere (a turned section, a frame read by quadrature): there it never refuses and reports the run unbounded by name when a fold cannot be excluded. A fold between two far-apart parts of the run is not detected.
An extrusion of a polyline profile with runs of collinear vertices avoids degenerate cap triangles whatever its placement whenever a triangulation avoids them, and a cap triangle that is a sliver and flat to rounding is refused by name (#415, in
axiolid-construct).A run of sections along a curve that is itself placed at a station whose frame is not exact is unbounded by name (#408), where it was certified against the read placement rather than the exact one, a run of 130 or more sections included. Its folds are still decided, on the frames as read, and refused. Requires
axiolid-evaluate0.3.20 forFrameLaw::atomic_placedandFrameLaw::with_placements_read_as_exact.A boolean's deviation report (and the exact union referee's, #391) covers the boolean of the operands as read (#421), each moved by at most the named readings, which may change the topology against the operands as given: the furthest any kind of reading within tolerance moved a feature (an angle turned counts as a distance over the operands' extent) and the report's rounding floor are added to the measured distance, not combined with it by worst, and are named as contributions of their own next to the coincident supports'. A report whose merged points, contacts or incident points moved further than its coincident supports no longer yields a bound below the move. The bound of a boolean measured against an exact result grows by those two terms.
A closed B-spline edge whose span crosses the seam past a C0 knot such as 0.1, 0.2 or 1/3 keeps its chord bound: the copy of the knot one period on is mapped back to the knot exactly, not by
(t + period) - period, which left the piece ending an ulp past the knot (#426).A curved face's Delaunay flips (#425) visit interior edges in key order, not in a
HashMap's: compiling the same B-rep twice gave different triangles (a cap bounded by a circle, whose corners are cocircular) or the same triangles in a different order (a unique triangulation), from call to call and process to process. Each flip's decision is unchanged.
0.3.43 - 2026-11-03
Fixed
- A mesh union whose result holds two bodies within the linear tolerance of each other (faces in contact under a general rotation, or a gap smaller than the tolerance) is refused by name,
GeomError::Degeneratestarting withUNMERGED_UNION_CONTACT, instead of returned as two disjoint solids (#356, #357). A union whose operand already holds two shells within the tolerance is refused too. - A boolean of operands placed by the same linear part (a rotation and uniform scale) is cut in their shared frame and the result placed, so faces in contact under a general rotation merge (#356), and a flush opening under a turned placement shared with its wall cuts through; the world-frame cut remains the fallback. A snap in that frame is reported with the placement's rounding added.
Changed
- Depends on
axiolid-inspect(surface_gap) in production (ADR 0096). - A union reads its operands as touching within the linear tolerance: faces of the two operands on a shared axis-aligned plane within the tolerance are set onto each other, so such a union fuses into one body, adding at most the tolerance of material. The move is reported under
SNAPPED_OPERANDS(#357, ADR 0096). Differences and intersections keep the rounding reach (#291).
0.3.42 - 2026-11-03
Added
- Runs of sections at stations along an intrinsic plan report a certified deviation (#361): a clothoid, or lines, arcs and transitions in one piecewise law, alone, level in space or under an elevation (a gradient curve), in the section and the plan frame. The bound rests on the curvature law's closed-form derivative suprema and a certified position table (
axiolid-evaluate0.3.19). A 5 km alignment at a 1 mm chord error compiles and certifies in about 370 ms. TheUNCERTIFIED_FRAMEStext no longer names intrinsic curves in general: chains, banked curves over an intrinsic plan, intrinsic space curves with torsion or tilted, and offsets beside intrinsic curves stay unbounded by name.
0.3.41 - 2026-11-03
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
Fixed
- A box less a torus whose tube leaves through its top face (#381), and a column united with a revolved bend whose cap lies in its foot (#382), compile exactly at a micrometre; requires
axiolid-brep-boolean0.1.13.
0.3.40 - 2026-10-29
Changed
- The exact boolean takes any operand the exact compiler compiles exactly, as subject or as tool, read through its placements: an extrusion, a revolution (its cylinders, cones and tori), another boolean, or whatever else it compiles exactly, instead of refusing all but a placed extrusion ("exact boolean operand that is not an extrusion", "exact boolean tool that is not a placed extrusion"). A box united with a quarter-turn revolution of a circle is certified by
compile_mesh_with_deviationwithin the budget; its difference and intersection are measured like an extrusion's, and an oblique cut of faceted operands can exceed the budget (#380). The measurement covers the torus faces as it does planes and cylinders. An operand the exact compiler refuses is refused with its own family named ("primitive", "tapered revolution", "polygon mesh", "half-space", a full-turn profile touching the axis). A tool that is a boolean is exact now too: the tool is compiled exactly, and what cannot be compiled is refused by name (#326's triage decision). Its half-space-intersection example is still refused bycompile_clip(#378). - The deviation search no longer stops at 10% above its lower bound when that gap straddles the requested budget: it keeps tightening while the bound straddles the budget, within the cell cap and the resolution floor (it can still stop above the budget), so a mesh whose true deviation is within the budget is usually certified within it (a torus' tessellation at
1e-3measured0.95e-3and was reported1.004e-3) (#378).
0.3.39 - 2026-10-29
Changed
- The exact compiler builds the union and the intersection of placed extrusions (and of placed booleans, as subjects), as it did the difference, through the general boolean; the deviation report certifies their meshes against that result like a difference's, the same measured bound, instead of
Unbounded("exact union or intersection of placed operands"), so a circular column united with a box at chord1e-3reports a Certified bound within the budget. A tool that is itself a boolean is still refused by name (#326). An intersection that leaves nothing isDegeneratenaming its operator, no longer "difference" (a union cannot leave nothing). A symmetric difference of placed operands is refused by name (#376).
0.3.38 - 2026-10-29
Fixed
- A fixed-reference sweep along a chain that ends at its start with a corner is refused by name instead of being swept open with crossing end caps (#375).
0.3.37 - 2026-10-29
Added
- A fixed-reference sweep along a tangent-continuous chain of segments and circular arcs, open or closed, is proven within the chord budget (the profile's share plus half the budget for the walls, none along one segment) when the reference is normal to the plane of every arc or lies in it; the twist of a reference that only nearly is comes out of the walls' half, so the sum stays within the budget; the deviation report names it "segments and arcs" (#122). An arc whose twist is more than an eighth of the budget (oblique to the reference) is still swept as sampled and reported unbounded.
Changed
- A fixed-reference sweep refuses a corner in its directrix by name, as the format requires a tangent-continuous directrix (#122). A closed chain proven by the segment and arc path is swept without caps; a twisting one is still swept as sampled and capped. The deviation detail of a proven segment or arc is now "segments and arcs".
Fixed
- A swept disk wider than the bend of a lone arc it follows is refused instead of meshed into a tube that folds through the bend's axis (#122).
- A fixed-reference sweep along a circle whose frame is not orthonormal (an ellipse in all but name) is reported unbounded, where it was reported proven although it is swept as sampled (#122).
0.3.36 - 2026-10-28
Fixed
- A mesh
Revolutionof a contour with an arc ending on the axis (a half- or quarter-disc) compiles closed with one vertex per pole, in a full and a partial turn, instead of being refused as a pinch (#365); the flattened profile now ends segments at their exact joints (axiolid-construct).
0.3.35 - 2026-10-28
Changed
- A mesh
Revolutionwhose profile touches the axis now compiles to a closed mesh instead of failing withUnsupportedInput(#363, fromaxiolid-construct), unless a full turn touches the axis at an isolated vertex, which pinches the solid and staysUnsupportedInput; aTaperedRevolutiontouching the axis is still refused (#364).
0.3.34 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.33 - 2026-10-28
Fixed
- A mesh
RevolutionorTaperedRevolutionwhose profile crosses the axis now fails withInvalidInputinstead of compiling a self-intersecting solid, and one touching it withUnsupportedInputinstead of compiling a degenerate solid, for full and partial turns alike (#351, fromaxiolid-construct; proper meshing of touching profiles is tracked in #363).
0.3.32 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
Fixed
- An edge without a p-curve that leaves its B-spline face's surface by more than the linear tolerance is refused by name ("leaves its B-spline face's surface"), where the certified search ran out of budget refining toward the parameter resolution and reported
BudgetExceeded(#320, ADR 0086 amendment). A sample whose strict search overruns its budget is retried at the model tolerance and accepted when that pass proves it unique.
0.3.31 - 2026-10-20
Changed
- A B-rep face without a surface reports its slab width whenever its corners are not exactly coplanar (#343, ADR 0080 amendment 2026-10-20). Within the linear tolerance of its fit plane it reported a proven
0, so faceted IFC walls whose faces lie up to4e-10m off their plane read as exact. Coplanarity is now decided in exact arithmetic on the stored corners (certifiedorient3d), and a face that fails it reportsCertified(w)under"non-planar face without a surface", the width #261 computes, however small;0now means planar. An exactly coplanar face, tilted or not, still reports0; a face warped beyond the tolerance reports what it did, and authored polygon faces keep their tolerance rule.
0.3.30 - 2026-10-20
Added
- B-rep edges without a p-curve whose B-spline or polyline curve does not already run from vertex to vertex are trimmed at their vertices (#333), which were refused by name. A B-spline locates each vertex by the certified global projection (
axiolid_nurbs::project_curve3_certified, at a thousandth of the tolerance), its minimiser cover refined to a fixed parameter resolution by certified speed bounds, and requires that cover to be one connected interval: a vertex whose certified distance lower bound exceeds the tolerance is refused as off the curve, one the curve passes more than once as ambiguous. A polyline locates each vertex on its segments exactly, under the same uniqueness rule. On a closed curve (a closed polyline, a B-spline whose closed seam verifies) the edge runs forward from its start vertex across the seam where it must, and is the whole curve from a single vertex; two vertices at one point are refused as ambiguous. Section curves stay refused by name. A span over a polyline corner, a B-spline's full-multiplicity knot or a closed curve's seam is now bounded piecewise, the split points' distance from the span's chord added, where its chord bound was missing before.
Changed
- The "result touches itself" refusal (#194) is shown to refuse only true contacts (#328, #327); no code path changes. Chains of half-space intersections, each plane through an edge an earlier cut left, compile to their analytic volume under placements, and a seeded sweep of such chains is never refused. The refusal openbimrs/ifc met is a non-convex subject: a wall notched by a sloped clip, cut by a plane through the notch's corner, is two solids meeting along the corner line; the same strip cut with its prism's walls first compiles to its analytic volume. The box tangent to a curved wall's inner face (#327) touches it along a ruling of the chorded face exactly on the tangent plane; a unit in the last place past it, the box crosses the ruling by under
1e-15and the slit that opens, about1e-13wide, is returned closed: both stay refused.tests/half_space_chain_contact.rspins the chains and the notch; probescripts/probe_pinch_mutants.py(7/7 killed).
Fixed
A boolean's deviation measurement admits an exact face whose pcurve loops close only within the readings the exact boolean took (#334), up to the largest linear one it reported (
BooleanReport::linear), never beyond the caller's linear tolerance, in parameters by the surface's largest first derivative. It refused such faces by name ("an exact face's loops do not close in its parameters"), so a flat wall whose openings' faces coincide with its own or each other's within1e-8was left unbounded. The slack holds the gap measured, so the bound stays sound, and loops that close exactly keep their rounding gap: the all-planar bound of #314 is unchanged. A synthetic wall5 x 0.25 x 3less a door and windows whose caps and shared side are1e-8and3e-8off is certified at about its readings (2e-8,6e-8), its exact volume checked; at0it stays at rounding (1.3e-12). Probescripts/probe_closing_reading_mutants.py(5/5 killed).A B-rep whose planar face shares a straight edge with a curved face closes (#336): a D-shaped half cylinder (a half-cylinder wall and a planar face sharing two rulings, with two half-disk caps) compiled open, since the wall sampled each ruling in pieces and the planar face took only its endpoints. Every edge a curved face uses is now sampled once, before any face, from the strictest requirement of all the faces using it (the largest of their power-of-two span counts), and every face takes those samples, a planar face's straight edges included. The mesh no longer depends on the order the shell lists its faces in. A planar face that takes samples adds their distance off its plane and off their edge to its bound.
0.3.29 - 2026-10-18
Fixed
- Requires
axiolid-reference0.3.9, and through itaxiolid-evaluate0.3.17, whose certified frame laws and intrinsic positions 0.3.28 uses. 0.3.28 acceptedaxiolid-reference0.3.8, which accepts anyaxiolid-evaluatefrom 0.3.12, so a lockfile holding an older one did not build.
0.3.28 - 2026-10-18
Added
PolylineCompilercompiles trimmed, composite, offset and intrinsic curves (#317), which it refused by name, so anOpenProfilewhose path is such a relation compiles too. A trim of an atomic 2D or 3D curve is the basis flattened between the trim parameters with its own chord bounds, the parameters read as a station basis reads them (a closed conic wrapped from the start selector by the trim's sense, across its seam), in the trim's sense; a parameter past a non-periodic basis's domain is refused. A composite is its segments compiled to the budget in their senses, each joint one shared vertex (the earlier segment's end, and the first vertex where it closes), the bound the worst segment's plus the largest joint gap within the linear tolerance (the segments compiled once more within the budget less the gap when that misses it); a larger gap is refused by name. A constant offset (2D: the left normal; 3D:V x T) of an atomic curve, a trim or a composite takes its basis's parameter, offsets piece by piece on the side each piece's direction gives, and is bounded byh^2 / 8 sup |P''|from the basis's derivative bounds through the unit-vector rule of the certified station lengths (#294, #307), cut where the basis is notC^2. A 2D intrinsic curve, and a 3D one whose torsion law is identically zero, is bounded by its curvature law (h^2 / 8 sigma sup |k|), across seams of a piecewise law, and its vertices are certified positions (bound::IntrinsicPositions, an eighth of the budget, the largest vertex error added to the bound), never the evaluator's quadrature estimate; an offset beside a 2D one is cut at its seams and read the same way. Still refused by name: implicit and banked curves, a 3D intrinsic curve with torsion, a trim of a relation or a placed curve, an offset of an offset, of a placed curve, of a chain, implicit or 3D intrinsic curve or across a corner of its basis, a 3D offset without a reference direction, offsets by stations, and surface and parameter curves.FINER_FACTOR, re-exported fromaxiolid-brep-booleanwith the report it describes (BooleanReport::finer, #332).- Certified deviations for runs of sections at stations whose frames are read by quadrature (#316): along an ellipse, a B-spline, and an offset beside a line, a circle, an ellipse or a B-spline, constant or by distances. Such a run reported
Unbounded(UNCERTIFIED_FRAMES); its cells are now bounded against the exact frame law (axiolid_reference::station::FrameLaw, its quadratures certified to a thousandth of the run's budget), each vertex charged the read's certified distance error, point error and axis error on top of the chord bound (thestation::boundmodule documentation). Runs along the openbimrs/ifc#443 deck's offset beside an ellipse, a tapered run along a B-spline and runs along an ellipse and its offset are certified within a millimetre request, and a dense sampling of the exact solid, framed independently of the kernel, stays within the bound (tests/station_bound_quadrature.rs). The deck's own run, which crosses the corner its widening law makes at station 2, is still unbounded byMITRED_SECTIONS: a cell beside a mitre on a curved piece is not bounded. Intrinsic curves, chains and offsets beside them, skewed circles and steep tangents stayUNCERTIFIED_FRAMES, whose message now names them.
Fixed
- An edge without a p-curve is bounded by its vertices, not by its curve's domain (#319). Each vertex is located on a line, circle or ellipse by closed-form inversion, its residual checked against the linear tolerance: a line is the segment between the two parameters (an
IfcLinewith a unit-length vector no longer refuses with "ends 1.999e0 from its vertex"), a circle or ellipse the arc forward along the curve from the start vertex to the end vertex, and the whole turn from an edge's one vertex, wherever the curve's own parameter starts. The shared samples and the deviation report cover that span only. A vertex off its curve, two distinct vertices at one point of a closed curve, and any other family whose domain does not already run from vertex to vertex (a B-spline, say) are refused by name. The edge runs in its curve's sense fromEdge::starttoEdge::end: an exchange format's edge that runs against its curve (IFCSameSense = .F.) is that edge with its vertices swapped, used the other way. - Curved walls whose openings abut, one's side reaching a few micrometres or just under the tolerance past its neighbour's, are certified (#332): the general boolean refused the cut, so the deviation report left the boolean unbounded; it now cuts it again at a finer tolerance (
axiolid-brep-boolean). A wall of radii6to6.25less five wedge openings, abutting by15 umand0.6 mm, is certified within the millimetre requested, its exact result at the closed-form volume. - A box touching a curved wall's inner cylinder along a ruling compiles exactly at the closed-form volume (#327). Its mesh, whose material is as thin as nothing along the contact, stays refused as a result touching itself (#194) on the tangent plane and a unit in the last place past it; a unit short of it, the box is certified.
- A piece of a composite basis after an ellipse, a B-spline or a numerical offset carried no error for where its start truly lies: the kernel sums the estimated lengths before it, and the run's measure now adds that sum's distance from the certified joint (#316).
0.3.27 - 2026-10-16
Added
deviation::COINCIDENT_OPERANDS(#324): a boolean whose exact result read faces of its two operands as one surface within tolerance reports how far that reading moved an operand, as a certified contribution of the boolean path next to its measured one, as a snap's move is reported. Such booleans now carry two contributions instead of one.- Bounded half-spaces along their placement's
z(#321,SolidOperation::BoundedHalfSpaceAlongZ), in both compilers: the kept side of the plane within the prism of the boundary along the placement'sz, at any angle to the plane, the boundary atplacement * (u, v, 0)as written. The mesh compiler builds the oblique slab (its boundary chorded within the budget over the frame's longest axis); the exact compiler builds the prism in the boundary's own plane, a margin past the subject's envelope both ways, and intersects it with the plane's side over the envelope, so an arc is a cylinder wall the plane cuts in an ellipse, and the mesh's deviation is certified against it. The IFC2X3 gable wall of openbimrs/ifc#515 (four sloped planes over vertical prisms) is 49.999912042537 m³ exactly, IfcOpenShell's volume (tests/bounded_half_space_prism_axis.rs). An axis within the angular tolerance of the plane and a degenerate placement are refused by name in both. Under a sheared placement, whosezleans off the boundary's plane, a boundary with arcs sweeps elliptical cylinders, which the exact general boolean may refuse to sew, by name.
Fixed
- The mesh compiler refuses a bounded half-space with no subject to size it by (#325) -- a root, a union or exclusive-or operand, a collection member -- as
GeomError::UnsupportedInput { input: "bounded half-space without a subject to size it by: ..." }, as the exact compiler already did. It used to mesh a slab twice its boundary's extent deep, an arbitrary stand-in for a solid unbounded on its kept side: silently wrong output, now refused by name, as #295's refusal was. - A curved wall less a niche flush with its inner face, or one unit in the last place inside it (#324), is certified: the general boolean's refusal ("exact boolean whose kept faces do not sew into a solid") left the deviation unbounded. A quarter ring of radii
4.8to5less a niche of10degrees reports0.99973 mmat a1 mmrequest, as the niche placed well inside does, with the coincidence reported (at most4e-15), and the exact result measures to the annular sectors' volume. - A bounded half-space's slab is sized by the subject it meets (#325), along either axis: the tool of a difference or an intersection, read through any instances above it (their placements composed, the subject brought into the tool's frame), reaches past the subject's farthest point on the kept side by a tenth of its span. It was a slab twice the boundary's extent deep, so a small boundary over a tall subject (a 0.5 m square over a 20 m column) cut it too little, or kept too little, with no error. An unbounded half-space's tool is now read through instances too. The IFC2X3 gable wall compiles to 49.999912042537 m³ in the mesh compiler as well, certified against the exact result (
tests/bounded_half_space_subject_depth.rs,tests/bounded_half_space_prism_axis.rs). - A bounded half-space's placement is read whole (#322), in both compilers and along either axis. Along the plane normal the mesh compiler normalised the placement's axes and rebuilt
yfrom the normal, so a scaled frame cut its boundary at unit scale, a sheared one unsheared and a left-handed (mirrored) one its mirror image, with no error; the exact compiler did the same. Now the boundary keeps the frame's scale, shear and handedness (BoundaryPrism::along_plane_normal), and only a degenerate frame (parallel or zero axes) is refused by name. A right-handed unit frame, even one whosezpoints against the normal, reads as before. In the exact compiler a profile boundary under a scaling or mirroring frame is a derived profile, its arcs exact under a similarity and refused by name as ellipses otherwise (tests/bounded_half_space_whole_frame.rs).
0.3.26 - 2026-10-14
Added
ReferenceMeshCompilerimplementsPolylineCompiler(#311): aCurve3, aCurve2(lifted toz = 0), anOpenProfileand anInstanceof any of them, or anInstanceAtStationon a line basis, compile to a 3D polyline. Each smooth piece of the curve (cut where it may not beC^1) is bisected until every span's certified chord bound, the exact sagitta of a circular arc orh^2 / 8 sup |c''|from certified derivative bounds, fits the chord budget; the largest is the reported bound, so a dense sampling of the exact curve never lies farther from the polyline. A placement compiles its source to the budget shrunk by its stretch and grows the bound by it, as an instance's mesh deviation does. Refused by name: unbounded lines, families whose flattening is not certified (intrinsic, implicit, banked), curve relations, collections, and curves at numerically resolved stations.compile_meshstill refuses curve nodes withCurveEvaluation.- Surfaces trimmed by a parameter rectangle (
SurfaceRelation::RectangularTrimmedover a plane, cylinder, elliptical cylinder, cone, sphere, torus or B-spline surface) compile throughcompile_meshas sheets reportedMeshClosure::Surface(#311), facing the trimmed surface's normal (turned round when exactly one sense runs against the basis), wrapping past the seam of a periodic parameter. A uniform grid of exact surface points, each cell within1/8 ((A + B) h_u^2 + (C + B) h_v^2)of its triangles from the surface's certified interpolation bounds over it, sized to the chord budget;compile_mesh_with_deviationreports it underDeviationPath::TrimmedSurface(new), the surface family as the detail, and a cell a B-spline crease crosses as unbounded by name. Refused by name: a trim of a surface relation, an empty range, one longer than a period, one against its sense on a parameter that does not wrap. An atomicSurfacenode is unbounded and still refused. - A B-rep of faces that belong to no shell or solid compiles as one surface model (#311,
MeshClosure::Surface), each face as authored; a B-rep with no face at all is still refused. DeviationPath::Placement,deviation::PLACEMENT_ROUNDING,deviation::PLACEMENT_EPSILONSanddeviation::BUDGET_BELOW_PLACEMENT_ROUNDING(#295): the rounding of placing an instance's vertices, named in the report, and the input family a budget below it is refused with.- Curved B-rep faces whose edges carry only 3D curves compile (#310, ADR 0086); they were refused with
Unsupported { Tessellation }. As IFC writes them (IfcEdgeCurvewithout anIfcPcurve), the face's parameter boundary comes from inverting the edge's shared 3D samples onto its surface: in closed form on planes, cylinders, elliptical cylinders, cones, spheres and tori, and byaxiolid-nurbs's certified inversion on B-spline surfaces. Shared edges keep one set of vertices, periodic parameters unwrap along the ring, and the deviation report adds each span's chord bound, the surface image of its parameter chord and the lens between them. Refused by name: a sample at or within tolerance of a cone apex or sphere pole, an edge that turns a quarter period between samples, a sample off the surface or without unique B-spline parameters, and an edge curve whose domain does not run between its vertices. Edges with pcurves keep their path. As on the pcurve path, a face's report adds its triangle bound and its trim terms, so the report may exceed the request; checkmeets_requested. The crate now depends onaxiolid-nurbs. - Planar B-rep faces bounded by curved edges without pcurves compile (#310): a face on a declared plane with an edge that follows a curve (an IFC cap bounded by a circle or an ellipse) is meshed as a curved face on its plane, its boundary the edges' shared samples inverted onto the plane with a residual check, so a cap and the wall across its rim share the rim's vertices by index. Its report is the edges' chord bounds plus each sample's distance from the plane, where it was chorded vertex to vertex and unbounded (or refused, for a rim of one vertex). A closed cylinder written as an IFC advanced B-rep (a seamed wall and two capped rims, no pcurve anywhere) now compiles as a watertight
MeshClosure::Solid.
Changed
- An all-planar boolean's measured deviation is reported near rounding, not near the request (#314). The search over the exact result stopped once its worst cell was within the budget, and a cell across a trim edge was bounded whole, overhang past the edge included, so the bound only shrank with the cell's size: the wall
3.65 x 0.25 x 3.67less two doors reported9.96e-4at a1e-3request for a mesh whose certified volume is exact to1e-14. On a plane, a boundary cell is now bounded over its part within the slack of the face (the cell clipped by the mitred offset of the one edge or corner it meets, as convex pieces), the slack is the chord bound the trim flattening actually reached (rounding, for straight trims), and when every face of the result is a plane the search runs to within 10% of the sampled maximum or to 1024 ulps of the mesh's scale, with no budget to stop at. That wall now reports1.18e-12(sampled3.9e-14), and its report costs no more than before (benches/boolean_deviation.rs, mesh and report: 378 ms before, 358 ms after). Curved patches are bounded and searched as before; a curved boolean's planar faces are clipped too, which only tightens their cells (the wall with a round window: 488 ms before, 392 ms after). The budget still settles an all-planar search once a real deviation is shown, a sampled lower bound or a flattened warped operand's slab width (#266) above 1000 times the rounding, so a mesh that is off its exact result costs no more than before (a wall with a moved mesh corner: 0.39 s before, 0.10 s after; the #266 tests at or under their times), and one at rounding is never stopped by the budget.
Fixed
- Deviation bounds of instanced bodies include the rounding of applying the
f64transform (#295, ADR 0080 amendment). An instance moved its source's vertices by a few ulps of their coordinates, about1e-9at6e6, and the bound grew only by the relative stretch, so a mesh placed far from the origin could lie further from the exact surface than itsProvenorCertifiedbound. Every bound of anInstanceorInstanceAtStationnow adds4 f64::EPSILON M,Mthe largest magnitude a placed coordinate sums (derived indeviation::placement_rounding; an identity or a mirror adds nothing), and names it underDeviationPath::Placement. The source is meshed with room for it (the floor at the translation's scale plus a2^-20share of the budget, and once more with room for what its mesh showed where that was not enough), so a request above the rounding is still met; one at or below it is refused withBUDGET_BELOW_PLACEMENT_ROUNDING. A torus at(6e6, 6e6, 100)with a1e-9budget is refused; with2e-8it reports within the budget, about a quarter of it the rounding, and dense samples of the exact torus against thef64mesh stay within the bound. Instanced bounds are now up to a2^-20share below the budget rather than at it.compile_polylinethrough anInstanceorInstanceAtStationfollows the same rule (term, room, refusal): a polyline placed at6e6reports4 eps (6e6 + 4)where it reported0.
0.3.25 - 2026-10-14
Added
- A test of the deck of openbimrs/ifc#443 (#307): rectangles at stations 0 and 5 along an offset by distances beside an ellipse, windowed by
1e-5, compile to the window-free deck bitwise (about 10 by Pappus), and stations 30 along and within the window of the joint after the varying piece resolve, throughaxiolid-evaluate's certified lengths of varying offsets.
Fixed
- A boolean cut in its host's own frame and placed far from the origin no longer keeps a skin its placement folds (#312, ADR 0080 amendment). The snap closed residues only up to
16 f64::EPSILONof the operands' own coordinates (about1.3e-14for a 3.67 m wall), so doors2e-14to1e-10short of a wall's face left a skin that thin, and once anInstanceplaced the wall 5600 km out its faces crossed andenclosed_volumerefused the mesh as self-intersecting. The reach now covers the coordinates the result is placed at:InstanceandInstanceAtStationcarry the placed coordinate scale down to their source with the tolerance and the chord budget, and the snap reaches16 f64::EPSILONof it (about2e-8there), still capped at the tolerance and still landing only exactly; a micrometre skin stays. The compile cache keys on that scale, rounded up to three significant bits, so a source placed near and far compiles once per scale. The move is reported underdeviation::SNAPPED_OPERANDSas before.
0.3.24 - 2026-10-13
Added
- A boolean with a warped operand reports a certified bound that covers the warp (#266, ADR 0080 amendment).
compile_mesh_with_deviationmeasures a boolean whose operand is an authored polygon or triangle mesh, or a B-rep of planes and warped faces without a surface, against the exact boolean of its operands flattened to their triangles (each within its slab width of every reading of its warped faces, #261), by any operator, and reportsDeviationPath::Booleanunder "measured against the exact boolean of operands flattened within their slab width, plus it" with the measured distance plus the largest slab width; the search settles within the budget less that width, someets_requestedholds exactly when the widths and the measurement fit the budget. Each warped operand's own contribution ("non-planar authored face", "non-planar face without a surface") travels with the boolean, as a snap does. An unwarped mesh operand is measured against the exact boolean of the operands as given. Where the exact boolean cannot take the flattened operand (a mesh that does not close, a B-rep with a curved face, a tool that is itself a boolean, a configuration the general boolean refuses) the boolean stays unbounded by that refusal's name. These booleans were unbounded before ("exact boolean operand that is not an extrusion");ReferenceExactCompileritself still refuses mesh operands by name. - Certified deviation for runs of sections at stations (#247, ADR 0082 amendment):
StationedSpine,SectionsAtStations,SectionedSurfaceandOpenSectionsAtStationsreportDeviationBound::Certifiedinstead ofUnboundedwherever the frame law along the basis has certified derivative bounds (lines, polylines, circles, elevated and banked curves with line or circle plans and closed-form profiles; section and plan frames; placed and reversed composite pieces). Each mesh cell is bounded by its vertex paths' chord boundh^2 / 8 sup |A''|, breakpoints at the seams inside it, the wall quad'sloft::quad_deviationand the arc-length inverse's slack; the stations are refined until the bound fits the budget. A constant section on straight legs is bounded through its mitres. Unbounded by name: a numerically framed basis, a cell beside a mitre on a curved piece or with a changing section, and different curved profiles. ReferenceExactCompilercompiles aStationedSpineorSectionsAtStationsexactly where it has a closed form (#247): a constant section along a line or a straight polyline (an extrusion, oblique under a tilted orientation), along a horizontal circle (a revolution), and a linear taper of straight-edged sections along a line (a planar polyhedron); every other run is refused by name.
Changed
- The stations of such a run are refined until the certified bound fits as well as the sampling test, so a curved run may mesh finer (#247). The exact compiler now reads a stationed spine's stations before refusing it, so a station beyond the basis is refused as the mesh compiler refuses it.
Fixed
- A trim of a B-spline by points compiles (#306, ADR 0082 amendment): a graph trim's point selector on a 2D or 3D B-spline, which the closed-form inversion refused, is resolved by the inversion a curve path's point bound takes (#304,
StationCurve::parameter_at_bound): the same point-bound tolerance, sampling, Newton refinement and merging of places, for station bases, sweep directrices and their pieces alike. The trim therefore flattens bitwise to the piece bounded byPathBound::Pointat those points, with and against its sense. A point off the B-spline, or one it passes through twice, is refused namingPATH_POINT_OFF_CURVEorPATH_POINT_AMBIGUOUSunless the trim falls back to a parameter selector beside it. - A keyhole face's seam is never a triangle edge of a shell (#283, ADR 0083 amendment). Planar B-rep faces and authored polygon faces are triangulated by
planar::clip_shell_faceover construct'striangulate_shell_face: where the clipper made the seam of a keyhole ring (#270) a triangle edge, it is flipped away or split off at a Steiner point inside the face. Faces that run along the same segment (a rim's two end faces at its cut) then pair along it alone instead of meeting four triangle uses of it, so a declared B-rep solid reads asSolidrather thanOpenSolidand authored polygons as a solid rather than a surface. A Steiner point is placed in space by the same chain of midpoints of the face's corners, so it lies exactly in a face in an axis plane; elsewhere within rounding, reported as the certified deviation"point added inside a keyhole face, off an axis plane"(pathBRepFaceorAuthoredMesh), bounded by the slab width of the face's corners and Steiner points about its fit plane. Like a warp's slab width (#266), it is carried by name through booleans, counts towards the width an operand is flattened by when a boolean is measured against its flattened operands, and a B-rep with Steiner points still counts as a polyhedron there. Each triangle at a Steiner point is re-checked, exactly, in the face's projection after placing it; the noise-band split is kept only where it puts no edge back on a seam. Curve-bounded planes keep the surface-patch path. Where no other face shares the seam, as in every valid two-manifold shell, the seam as a triangle edge was only a diagonal and those shells were already closed; they keep their closure and volume. Tests (tests/keyhole_shell.rs): a box whose top face is a keyhole into a pyramid pocket, wide and as the corpus's thin rim seamed at its corner, with and without a fin of two coincident faces hanging from the seam, as B-rep and as authored polygons, closed with exact volume and the seam used by no triangle of the face; the same tilted out of the axis planes reports its Steiner point. pinch::find, which refuses a boolean result that touches itself, welds positions by value (#283), so a corner written with-0.0meets its0.0twin, as #269 fixed in construct.- Swept disks along closed polylines and with corners beside an arc (#248). A closed polyline is swept as a loop by
axiolid_construct::pipe::swept_disk_along_loop(its closing segment included, a repeated first point dropped), mitred or filleted where it closes too; a parameter range on it, and a closed polyline inside a composite, are refused by name. Its deviation stays proven, under the detail"closed loop of segments and arcs", the closing twist included. A corner between a segment or an arc and an arc is mitred instead of refused: the rest of the pipe stays proven, and the bend's band of torus up to the mitre plane and the planar face between the two tubes' sections are certified against the mesh by branch and bound (cells trimmed by certain height, distance and azimuth tests), reportedCertifiedunder the new detaildeviation::MITRE_BESIDE_AN_ARC. Measured within the budget at 1 mm and 0.1 mm for corners of 20 to 120 degrees before, after and between bends, in and out of the bend's plane, solid and hollow.
0.3.23 - 2026-10-13
Added
- Offset runs continue to their basis's ends without a stated length (#303, ADR 0082 amendment): a station of an
OffsetsAtStationsat its basis's start (RunStation::at_basis_start) is read at0, one at its end (RunStation::at_basis_end) at the basis's length by the kernel's own measure, the lengthstation::curve_pathstates for the basis asCompositeBasismeasures it, bitwise (a line's parameter domain|d|), so the run reads exactly as the one stated at that number, swept as a directrix and flattened as a basis or a curve path. Resolution comes before the #301 snapping, so an end station's window snaps from the resolved end; resolved distances that do not increase strictly (a stated station at or past the basis's end) are refused by name asInvalidInput.
0.3.22 - 2026-10-12
Added
- Run stations snap onto seams within their windows (#301, ADR 0082 amendment):
SectionsAtStationsandOpenSectionsAtStationssections and the stations of the newCurveRelation::OffsetsAtStations(compiled as a sweep directrix and as a station basis, likeOffsetByStations) are each snapped by the #294 rule (station::window, the oriented station's own) before the run is read, and the run stands on the snapped distances; a window holding two seams or straddled by one is refused by name, and snapped stations that no longer increase strictly are refused asInvalidInput. An interior station snapped onto a joint where the tangent turns is the mitre's knot, its section the mitre section whatever side it names; the run's first and last stations keep their outgoing and incoming readings; an interior station on a seam whose tangents agree (a roll jump) is placed in the frame of the side it names. Window0andOutgoing, the defaults, are today's reading.
Changed
station::curve_pathdocuments that its pieces stay stated, each with the length the kernel measures, and that a caller without the graph who leaves the same pieces' ends open (#298) reads the same path (checked bitwise for an offset of a B-spline). Its output is unchanged.
0.3.21 - 2026-10-10
Fixed
- An instance of a body meshed to exactly its budget (a torus, a proven or certified pipe, a curve-bounded plane) no longer reports one ulp over the requested budget, which made
meets_requestedfalse (#258). The source was meshed to the budget divided by the transform's stretch bound, and that quotient multiplied back rounds above the budget at some factors (a scale of 0.33 or 1.19 at1e-4). It is now shrunk to the largest value whose rounded product with the stretch fits, as #255 shrinks a curve-bounded plane's, so rigid, scaled and nested instances report at most the request. The stretch bound itself is unchanged: a rigid transform keeps its1 + 96 epsmargin, since anf64rotation is orthonormal only to rounding, and scaled or sheared instances still grow the bound by their full stretch.
0.3.20 - 2026-10-10
Added
- An oriented station's seam-snapping window (#294, ADR 0082 amendment):
station::resolveandstation::placement, and so everyInstanceAtStationcompiled or used as a station basis, read a station within its window of exactly one seam of its basis on that seam, from itsSeamSide(its point the seam's), throughaxiolid_reference::station's snapping: exact seam positions where they are exact, certified intervals where a joint follows an offset, an ellipse or a B-spline piece. A window holding two seams, a seam straddling its edge and a seam whose distance cannot be certified are refused by name. A window of0, every existing station's, reads as before.
0.3.19 - 2026-10-10
Changed
- A difference whose tool is an oblique exact extrusion with arcs or a circle (a slab less an oblique round shaft or rounded opening) compiles exactly, and its mesh is certified against that result (#287). It was refused as "exact boolean over a curve or surface it cannot evaluate", and the mesh's report unbounded. So does an elliptical column crossed by a round bore, refused before for want of inverting a point on the column.
Added
station::curve_path(#290, ADR 0082 amendment): a graph curve a distance runs along, flattened into the neutralaxiolid_curve::CurvePatha station on it is measured along, for a consumer to read through the curve-evaluation contract'spath_*queries; an atomic curve is one whole piece, any relation a station refuses is refused by name. A station on the relation and the path read through the contract agree bitwise.- Offset curves as station bases (#289, ADR 0082 amendment):
station::resolve,placement,seams,curve_pathand every run of stations (spines, sectioned surfaces, offsets by stations) accept a constantCurveRelation::Offset(2D without, 3D with a reference direction) and anOffsetByStationsas basis, nested in composites and placements, measured in the offset's own length. A placement on an offset of a line is exact; on any other offset it is reported unbounded and refused by the exact compiler, as on any curved basis. A trim of an offset relation, an offset of an offset, a planar offset of a 3D curve and an offset across a corner of its basis are refused by name.
0.3.18 - 2026-10-09
Changed
- The mesh boolean's operand snap (#276) closes only rounding residues again: a coordinate moves at most
16 f64::EPSILONof the operands' largest coordinate magnitude (about1.3e-14for a 4 m wall, about2e-8at georeferenced coordinates), capped by the linear tolerance, instead of the whole tolerance (#291). Skins and slivers above that are kept as authored: a 0.1-1 mm skin under a 1 mm tolerance stays in the mesh, as it did before 0.3.16. On one model the wider snap took seven hosts' whole bodies, and walls' skins at near-flush slab openings. A1e-12-1e-9skin computed near the origin is now kept too; placed at georeferenced coordinates it may be refused as self-intersecting, as before 0.3.16. - The snap lands exactly or not at all: a coordinate is set onto a face of the other operand only where the face's three corners share it exactly (an axis-aligned plane), and every vertex with that coordinate value moves with it, so an operand's own axis-aligned faces stay planar (#291). Faces axis-aligned only within rounding are cut as given.
Fixed
- A slab extruded downward from its top, cut through by 22 openings flush with both faces, is meshed again with its closed-form volume, in its frame and placed at georeferenced coordinates (#291). Since 0.3.16 the snap moved openings' vertices onto planes within rounding of the slab's faces (a normal normalised to
0.9999999999999999, a frame turned by a rounded half turn, a profile edge's end evaluated asa + (b - a)), some landing above a face and some below, and the difference was refused as a void tangent to its host's face, or its volume as self-intersecting. - A snap never turns a working boolean into a refusal (#291): where the snapped operands' mesh boolean is refused or its result touches itself, the boolean is cut from the operands as given, and that result is returned with no snap reported; when both are refused, the refusal of the operands as given is returned.
0.3.17 - 2026-10-09
Changed
- A bounded half-space whose profile boundary keeps the side opposite the plane normal reflects the profile's prism in its
xaxis with its placement, instead of mirroring the profile as a derived profile first (#288, ADR 0084). The general boolean now cuts by a reflected cylinder wall as by one built directly, so the #277 workaround is gone; the clipped volumes are unchanged.
0.3.16 - 2026-10-09
Added
Bounded half-spaces whose boundary is a profile of lines and circular arcs (#277, ADR 0084).
ReferenceExactCompilerclips by the exact extrusion of the profile along the plane normal, each arc a right circular cylinder wall, so a wall clipped by the consumer's boundary (six segments, two arcs over a circle of radius 1.2) has its closed-form volume; for the side opposite the normal the profile is mirrored as a derived profile, as the polyline is.ReferenceMeshCompilerflattens the profile under the chord budget, and the clipped solid's deviation is certified against the exact result (#235), within the budget under dense sampling. Both compilers check the profile first and refuse, asInvalidInputnaming the fault, a contour that does not close (the flattener alone bridged the gap), an arc of no radius, a segment of no length, and edges that cross or touch other than neighbours at their joint. Polyline boundaries are unchanged.Stations along curve relations (#285, ADR 0082 amendment):
station::resolve,station::placementand every run of stations (offset curves, sectioned spines and surfaces) take a composite, a trim, a surface curve whose 3D curve governs and a curve placed at a station as their basis, nested and placed in any combination, 2D or 3D: the relation is flattened into spans of atomic curves (as a composite directrix reads it) and measured as oneaxiolid_reference::station::CompositeBasis. A station on a joint reads the piece itsSeamSidenames; a run across a joint whose tangents differ is mitred there. A placement on a composite is exact only where every piece up to the one read is an exactly placed line. Pieces measured differently, a gap, and an undeclared reversed piece are refused by name.station::seamsreads every relation a station can be measured along -- 2D relations, nested composites, trims of relations, placed segments, elevated pieces -- at the running sum of its pieces' lengths, with each piece's own seams inside it; a joint after an ellipse or a B-spline piece is refused as inexact by name.deviation::SNAPPED_OPERANDS(#276): the detail of theDeviationPath::Booleancontribution that reports operands snapped together within the tolerance (see Fixed), itsCertifiedbound the largest distance any operand vertex moved. It is carried through instances (scaled) and enclosing booleans, also where the enclosing boolean is measured against its exact result.
Changed
Oblique extrusions with arcs compile to the right exact solid (#280, through
axiolid-construct): a leaning circle or arc wall is an oblique circular cylinder, anEllipticalCylinderalong the direction, whereReferenceExactCompilerreturned upright cylinder walls a shear away from the mesh (and refused an oblique circle). The general exact boolean does not yet cut such a wall and refuses by name, so a boolean with an oblique round opening staysDeviationBound::Unboundedunder that name, never certified against a wrong solid (tests/boolean_deviation.rs). A face on such a surface is meshed within its reported bound (tests/deviation_report.rs).Behaviour change:
ReferenceMeshCompilerrefuses an extrusion whose direction lies within tolerance of its profile plane, as"extrusion direction in the profile plane"(#281, throughaxiolid-construct'sextrude_profile), where it returned a sliver or flat mesh; the exact compiler already refused it by that name.A station along a curve placed in a frame that tilts
+Z(#264) is no longer refused when the source is measured by arc length: its point and tangent are carried and its frame is the placed curve's own reference-up frame against+Z. An elevated or banked source so placed is still refused by name.The refusal of an unsupported station basis now names what is supported ("... a curve relation other than a composite, a trim, ...").
Fixed
A planar face no longer drops a corner lying within the noise band of a line through its neighbours (#278, ADR 0083 amendment). The sliver split after the certified clipper re-tested the band for the corners of the slivers it dropped; the band is not transitive, so a cap corner 0.4 um off
a-b, witha1 um offp1-b, ended up in no triangle of the cap while the side faces kept it, and a closed faceted prism meshedOpenSolid. The edge a dropped sliver leaves behind now takes exactly that sliver's corners, chained through slivers dropped next to it, and a split that does not bound the clipper's cover along the same edges is discarded for the cover. Every ring corner of a planar B-rep or authored polygon face is a triangle corner again.A difference whose tool stops a rounding error short of its host's face, or reaches that far past it, no longer leaves a skin or sliver that thin (#276). Kept as two faces
4.5e-15apart, such a skin crossed itself once the mesh was placed at georeferenced coordinates, and the volume kernel refused it as self-intersecting.ReferenceMeshCompilernow moves the tool's vertices within the linear tolerance of the subject's faces onto them, then the subject's onto the moved tool's, before the mesh boolean: by the smallest move onto every plane in reach, at most the tolerance, exactly onto axis-aligned planes, never folding a triangle, and not at all at a zero tolerance. The result is the boolean of operands perturbed within the tolerance (ADR 0080). A skin thicker than the tolerance is kept.
0.3.15 - 2026-10-09
Added
Nodes placed at stations (#264, ADR 0082 amendment):
station::placementresolves aGeometryNode::InstanceAtStationto aResolvedPlacement { transform, station, exact }-- its oriented station resolved asstation::resolvedoes, seam side included, and the rigid motion taking the source's localx,y,zonto the oriented tangent, left lateral and up.ReferenceMeshCompilermeshes a placed solid or surface like an instance; on a basis other than a line the placement is reportedDeviationBound::Unboundedunder the newDeviationPath::StationPlacement(appended last).ReferenceExactCompilerplaces a source exactly on a line basis and refuses any other by name. A placed curve sweeps as a directrix (a 2D source sampled in its ownz = 0, then moved), sampled rather than as exact pieces; an exact swept disk along it and a 2D relation as its source are refused by name.Stations along a placed curve (#264): a
CurveStation, anOrientedCurveStationand every run of stations may take a curve placed at a station (an atomic curve, or a placed one, as source) as their basis: the source's station carried by the placement, its seams unchanged (station::seamstoo). A placement whose frame tilts+Zis refused by name: there the carried frame is not the placed curve's own reference-up frame, and an elevated source's plan distance is not the placed curve's.station::seams(#263, ADR 0082 amendment): where a graph curve's seams are, from its stored data -- an atomic curve's (refusing a B-spline's corner knot, which only a quadrature locates), and a 3D composite or trim of lines, polylines and circles at the running sum of its pieces' lengths; any other relation and a 2D relation are refused by name.
Changed
- Stations on seams (#263):
station::resolvereads anOrientedCurveStationon a seam of its basis from the piece itsseamnames (a plainCurveStationthe outgoing one). Offset curves by stations, station-placed spines and sectioned surfaces read the outgoing piece at their first station and the incoming one at their last, and where they cross a seam whose tangents differ -- between two stations or at one -- place a section in its mitre plane (the bisector of the two tangents), sampling each side as its piece alone; a near reversal and an authored section the mitre would cut are refused by name, and a seam whose tangents agree is sampled as before. Their deviation is still reported unbounded by name.
Fixed
- Booleans with an operand extruded against its profile normal are exact and certified (#275).
ReferenceExactCompilerrefused an extrusion whose direction points down the profile normal (an opening cut down from a slab's top,ExtrudedDirection (0, 0, -1)) as"non-forward planar extrusion", socompile_mesh_with_deviationleft every boolean with such an operand unbounded. It now compiles them throughaxiolid-construct's mirrored build: the slab of the issue certifies the same bound whether its opening is cut up or down, and a round extrusion along-zclipped by a half-space is exact with its closed-form volume. A direction within tolerance of the profile plane is still refused, as"extrusion direction in the profile plane". - A ring corner inside a shared edge no longer opens a solid that is still reported
Solid(#265). Since #260 a planar face's certified clipper inserts a corner lying inside one of the face's own ring edges (a pocket rim touching the face's outer edge at an interior point, a hole touching another hole's edge) into that edge, but the neighbouring face sharing the edge kept it whole: a T-junction, with boundary edges, in a mesh reported as a solid. B-rep and authored polygon faces now collect those corners first, with the clipper's own exact tests (axiolid_construct::profile::ring_touches), and every face using such an edge takes them too, a triangle face included, so the shell is welded through them: closed, two-manifold and of the exact volume. No position is moved or added. - A mesh labelled a solid is checked to close (#265). A declared B-rep solid, an operation's solid and a boolean result are reported
MeshClosure::OpenSolid(new, inaxiolid-mesh-compile-contract) unless every edge is shared by exactly two triangles running it in opposite directions;solid_meshrefuses it by name, a collection containing it keeps the name, and a boolean refuses it as an operand. An empty boolean result still bounds the empty solid, and a B-rep declared a solid whose shell is itself open (a lone face) is no longer reported one. Faces the weld cannot reach (a curved B-rep face samples its own edges) are caught here rather than claimed as a solid. - Keyhole faces compile (#270). An authored polygon face or a faceted B-rep face whose one ring joins its hole by a seam run both ways (as
IfcPolygonalFaceSetexporters write a rim or basin outline) was refused as "its rings do not bound a region: profile outer ring overlaps itself"; a downstream triage found 216 such faces leaving 41 sanitary terminals unmeasured. Faces triangulate underPinchPolicy::Accept, which now accepts seams (axiolid-construct's Unreleased notes). Tests: the issue's keyhole, a seam from inside an outer edge and a thin rim, in every axis and diagonal plane, facing either way, from every start corner, authored and as faceted B-rep faces, with the exact area; the seam's corners named once and twice; a seam the ring crosses refused by name. - Planar faces that project with a
-0.0corner compile (#269). A faceted B-rep face is projected from its first corner onto in-plane axes; in a plane at 45 degrees between two axes a corner level with the origin lands at-0.0, andaxiolid-construct's clipper read the ring's orientation at that twin of its lowest corner and refused the face with "found no ear". The clipper now orders coordinates by value (axiolid-construct's Unreleased notes); a downstream triage traced 35 refused rectangular faces in two models to it. Tests: the issue's rectangle with a straight corner, one with straight corners on two sides and the same shape as a hole, in every axis and diagonal plane, facing either way, from every start corner, authored and as faceted B-rep faces, each with its exact area.
0.3.14 - 2026-10-04
Added
- Sweeps along elevated and banked directrices (#252): a
Curve3::Elevated(what a gradient curve lowers to) orCurve3::Bankeddirectrix, alone, trimmed or read through a curve-3D surface curve, is sampled by plan distance (its parameter, ADR 0082; a sweep range is a span of plan distance, and a line plan, which has no end, needs one) against its certified 3D chord bound, and swept as one smooth curve -- end tangents reported, refined until the walls fit -- unless its grade jumps at a profile or pivot seam inside the span (by more than 1e-9 rad). Swept disks, fixed-reference and surface-curve sweeps all take it. A disk's tube is certified against the exact tube (DeviationBound::Certified, detail "elevated curve" / "banked curve"); a grade break is unbounded by name ("elevated directrix with a grade break"); a straight plan under a constant grade is a segment, proven, and an exact swept disk (exact_directrix) along it. A banked curve's disk follows its rotation point; its rolled section frames remain those of station-placed sections. Previously every such directrix was refused asUnsupported { operation: CurveEvaluation }.
Changed
- A warped authored polygon face (#254) now reports the width of the slab its corners span about its fit plane, largest minus smallest signed distance over every corner, holes included, plus rounding (#261), instead of the largest corner distance from that plane. A warped face has no single true surface; either diagonal triangulation, the face flattened onto its fit plane and a bilinear patch all lie in that slab, so each is within its width of the mesh wherever it lies over the face. The largest corner distance understated how far two such readings can be apart. The bound grows by up to twice: a saddle with corners at
+-hreports2h(wash); a square with one corner lifted byhreportsh/2, the gap between its two triangulations at the centre (wash/4). StillDeviationBound::Certifiedunder "non-planar authored face", neverProven; faces within the linear tolerance of their plane are unchanged. - A disk swept along a smooth curve (ellipse, B-spline, and now elevated and banked curves) is certified with a search suited to a long tube's long, thin quads (#252): quads twisted by up to a twentieth of the budget are covered as flat regions (the thickness is added to the bound), each cell is cut the way that lowers its halves more (within an aspect ratio of 256), and the search settles once within the budget. A 60 m tube that exhausted the work cap at 4.5 times its measured deviation now certifies at 1.1-1.6 times it.
- A disk swept along a smooth curve whose certified bound lands over the chord budget is rebuilt finer and certified again (#252): the construction's chord shrinks by the overshoot (with a 0.9 margin), at most three times, keeping a rebuild only when it lowers the bound. The stations are placed on a second-order estimate, so bounds landed 4-8% over and
DeviationReport::meets_requested()read false; every elevated, banked and gradient-curve tube at 1 mm and 0.1 mm now meets it, at 6-54% more triangles where a rebuild was needed (none where the first mesh was within). A bound the rebuilds cannot bring within the budget is still reported as it is.
Fixed
- A B-rep face that declares no surface and whose corners are not coplanar reported
Proven(0)without being measured (#257), so a faceted box with one corner lifted 5 cm read as exact. Such a face is now measured as a warped authored polygon face is (#261): beyond the linear tolerance of its fit plane it reports the width of the slab its corners (holes included) span about that plane,DeviationBound::Certifiedunder the detail "non-planar face without a surface", andDeviationReport::meets_requested()follows from it. It is reported rather than refused: these faces come from faceted exports, and the exact compiler refuses B-rep nodes altogether. Faces within the tolerance, and faces with a declared plane (bounded against that plane as before), are unchanged. - Planar faces are triangulated by the certified ear clipper (#260). Authored polygon faces, curve-bounded planes, planar B-rep faces and curved faces' parameter domains used earcut, the triangulator that left #253's T-junction in profile caps (a triangle edge along a band of holes past their inner corners), and the planar B-rep path checked its output not at all. All of them now go through
axiolid-construct's clipper (profile::triangulate_with, ADR 0083 amendment), whose certificate proves the triangles tile the projected face exactly once with every ring edge a triangle edge; it replaces the authored path's area cross-check and backs theProven(0)and slab bounds of planar B-rep faces, which assume that cover. Faces are surface patches, so rings touching at a single point (#262: a hole touching the outer ring or another hole, an outer ring pinched at a vertex) triangulate underPinchPolicy::Accept; a pinch inside one face of a closed solid leaves every shell edge shared by two faces. A corner repeating its predecessor exactly is still dropped; every other corner is a triangle corner, so curved faces no longer put skipped trim samples back. Rings that bound no region are refused by name where earcut returned a partial cover:PolygonRefusal::AreaMismatchis nowPolygonRefusal::Rings, carrying the clipper's reason (crossing, overlapping, a hole outside). The noise-band split of planar faces stays, for slivers across corners a few nanometres off a straight run. Triangle sets differ from earcut's (a different ear order); areas, closure, authored corners and every deviation report are unchanged. A test fixture whose four cylinder edges each carried the whole closed trim as its pcurve traced its rectangle four times over, an overlapping ring earcut triangulated silently; it now gives each edge its own side.earcutis no longer a dependency of this crate or the workspace. Tests (tests/planar_certified.rs): #253's side-by-side and three-on-a-ray holes and 60 seeded random layouts become the top and bottom faces of prisms, authored and as faceted B-reps, which compile closed with volume area x depth and planar faces reportedProven(0); footprints touching at a corner and holes touching the outer ring or each other triangulate as single faces, authored and as surface models, with their exact area, facing up; a doubled corner inside a ring is dropped; the extrusion of a profile with a hole touching the outer ring is refused by name. Mutation probes:scripts/probe_polygon_faces_mutants.py(26/26 killed) andscripts/probe_closed_mesh_mutants.py(11/11, the split on the clipper's output).
0.3.13 - 2026-10-03
Added
Oriented and tagged stations (#246, ADR 0082 amendment):
station::resolvealso resolves anOrientedCurveStation, its frame turned by the orientation, its point andsectionnot. The newSectionsAtStationsspine andOpenSectionsAtStationssheet mesh like the #241 relations (same deviation paths, the exact compiler refuses the spine as a station-placed spine), each section's plane turned by its orientation, interpolated between sections in the base frame. Tagged closed sections are matched by tag: their profile must be a polygonal contour (optionally under a derived transform), each ring rewound outer counter-clockwise and holes clockwise, then re-started and re-ordered to line up with the first section's rings; a tag count that is not the vertex count, a curved or parametric profile, an outer ring tagged as a hole and an order that is no rotation are refused by name.A curve-bounded plane takes
CurveRelationboundaries (#255): aCompositeof lines, polylines, arcs and other segments, each read in itssame_sense, and aTrimmedbasis curve under everyTrimSelectorkind (parameter, point, arc length), resolved to points by the sweep directrix reader, 2D curves and point selectors lifted toz = 0. The boundary must close within the linear tolerance; an open one, a joint gap past it, an offset, surface-curve, parameter-curve or station-offset relation, and a 2D family other than a line, circle, ellipse, polyline or B-spline are refused by name. The deviation report bounds such a boundary by its leaves (straight exact, certified families within the chord budget, othersUnboundedby name) plus its widest joint gap. A joint or closing gap no wider than its ends' own rounding (8machine epsilons of the magnitudes they are computed from, as #250 welds profile joints:sin(2 pi)at a full turn,cos(pi / 2)at an arc's end) is welded and adds nothing, for atomic boundaries too. Boundaries are flattened to the budget shrunk by the plane frame's stretch bound, so a curve-bounded plane's reported bound stays within the budget andmeets_requestedholds.Stations (#241, ADR 0082).
station::resolveturns aCurveStationnode into its point and frame (xtangent,yup,zright). The reference mesh compiler meshesSolidOperation::StationedSpineas a closed loft andSurfaceRelation::SectionedSurfaceas an open sheet (MeshClosure::Surface), interpolating profiles and offsets linearly in distance between stations and refining between them until each section point's midpoint and each wall quad stay within the chord budget; anOffsetByStationscurve is sampled as a sweep directrix (its parameter is the basis distance). Their deviation is reportedUnboundedby name (DeviationPath::StationedSpine,DeviationPath::SectionedSurface), andReferenceExactCompilerrefuses a station-placed spine by name. Refused by name: a station on an instance or a curve relation, a distance beyond the curve, sections whose ring structure, vertex count or tags disagree, and an open section that is not a polyline.
Changed
- A sectioned surface whose section runs its tags in reverse is joined reversed instead of refused (#246).
- An authored polygon face off its plane by more than the linear tolerance is triangulated instead of refused as not planar (#254). It is projected onto its fit plane (the outer ring's centroid and Newell normal), ear clipped there and lifted back to its authored corners, which are never moved.
compile_mesh_with_deviationreports it asAuthoredMeshwith the detail"non-planar authored face", alwaysCertified, neverProven: the largest distance of any corner (holes included) from that fit plane, over the worst such face. That bounds the distance between the mesh and the face flattened onto the plane both ways, and is at least the largest corner distance from the plane the face was triangulated in. Faces within the tolerance triangulate and report exactly as before; rings that cross or enclose no area in the fit plane are still refused by face index, and the exact compiler still refuses polygon meshes by name. - A clockwise outer loop of a curve-bounded plane is reversed about its first point, so a loop read backwards triangulates exactly as the loop read forwards (#255).
Fixed
ReferenceExactCompilercompiles a round web hole touching the flange of an I-beam with root fillets (#249): exactly, with an empty report, atTolerance::ZEROwith exact axes, and reading the contact under a general placement. Its mesh is measured against the exact result throughcompile_mesh_with_deviationand reportedCertified(traced pcurves now have certified chord bounds). A hole a fraction of the tolerance off touching is refused by name ("... where the contact cannot be placed").ReferenceExactCompilercompiles placed differences whose round hole touches a planar face (#243): an I-beam whose web hole touches the flange, a wall whose round hole touches its top face, under any rigid placement and up to a fraction of the tolerance into or short of the face, and a column clipped by a plane a fraction of the tolerance into it (the #234 open item). Exactly tangent with exact placements the report is empty, atTolerance::ZEROtoo; otherwise it carriesPlaneTouchesCylinder. Throughcompile_mesh_with_deviationsuch a body's mesh is certified against its exact result instead of left unbounded. A curve crossing the contact where it cannot be placed is refused by name (BooleanError::UnsupportedContact).
0.3.12 - 2026-10-03
Fixed
- Restored: swept disks along polylines and composites with sharp corners compile again, mitred (#245). From 0.3.9 to 0.3.11 a
SweptDiskwhose directrix turned a corner with no fillet radius was refused ("give a fillet radius"); that was a regression. Each corner between two straight segments is mitred at half angle (both legs cut by the bisector plane, asIfcSweptDiskSoliddefines it), watertight, one winding, the volume exactly the ring polygon's area times the centreline length, and every point of the exact tube within the chord budget (measured: at most 0.50 of it at 1 mm and 0.1 mm for 30, 90 and 150 degree corners, out of plane, hollow).compile_mesh_with_deviationreports such pipesProvenat the budget. A cut through a polyline corner keeps the corner. Still refused by name: a mitre reaching past its leg, a reversal, a corner beside an arc, a disk radius equal to the fillet or bend radius (a horn torus), and a closed polyline, nowUnsupportedInput(its closing mitre is not built). The exact compiler still refuses directrices with corners.
Added
ReferenceExactCompiler::compile_exact_with_reportandcompile_exact_batch_with_reportscarry each body's rounding floor (BooleanReport::rounding_floor, #244): the largest floor of any general boolean or clip beneath the body, kept unchanged through rigid instances, and present on exact reports too.ROUNDING_FACTORis re-exported.
0.3.11 - 2026-10-03
Added
- Sweep directrices resolve
TrimSelector::ArcLength(#239) against an analytic basis, in the trim's sense, throughaxiolid_reference::arc_parameter; the mesh, piecewise and exact sweep paths read it like a parameter selector. An arc length on a curve-relation basis, or past the end of its basis, is refused by name.
0.3.10 - 2026-10-03
Added
ReferenceExactCompilerclips by half-spaces exactly (#234): a difference or intersection whose tool is aHalfSpace, or aBoundedHalfSpace(a polyline boundary in the plane), read through rigid placements, with the mesh compiler's semantics (agreementselects the normal side; the boundary is framed by its placement projected into the plane and swept along the normal). The half-space becomes a finite prism over a sound envelope of the subject plus a margin and goes through the general exact boolean under the #228 tolerance contract; the result does not depend on the margin (ADR 0080, amended). Clips compose with each other and with placed openings in either order: a wall minus its windows, clipped by two roof planes. Unions with a half-space, a half-space as the subject, a subject not built from placed extrusions, a boundary that is not a polyline, scaled placements and the general boolean's refusals are refused by name; a clip that removes everything isDegenerate.ReferenceExactCompiler::compile_exact_with_reportandcompile_exact_batch_with_reportsreturn each body with aBooleanReport(re-exported fromaxiolid-brep-boolean, withToleranceDecisionandToleranceDecisionKind) merged over every general boolean and clip beneath it (#236). An exact report means the body is the exact result of its operands as given and may be cited as exact; otherwise it bounds how far the operands were moved or turned, within the tolerance.
Fixed
Placed differences compile at
Tolerance::ZERO(#236): openings placed with exact axis matrices (entries0and+-1), through, blind or flush, give the exact difference and an exact report.A boolean's deviation contribution says when its bound also holds for the exact boolean of the given operands (#236): measured against an exact compiler result whose report is exact, its detail reads "the exact boolean of the given operands" instead of "operands within tolerance".
ReferenceMeshCompiler::compile_mesh_with_deviationreports a booleanCertifiedwhereReferenceExactCompilerbuilds its exact result (#235): differences of placed extrusions, such as a wall with a round window, an I-beam with round holes through its web, or a slab with a round shaft, and walls clipped by roof half-spaces (#234). The boolean's mesh is measured against that exact B-rep by the certified branch and bound of #232, over each face's trimmed parameter domain (pcurves flattened to a certified chord bound; a cell is dropped only when it is certainly outside the face). The bound is relative to the exact compiler's result, which is the exact boolean of operands moved by at most the tolerance (#228), and is not claimed for the boolean of the unperturbed operands; the contribution's detail says so ("measured against the exact compiler's result, operands within tolerance"). Nothing is derived from the operands' bounds, which are one-sided and say nothing about where the mesh boolean puts the cut. A boolean the exact compiler refuses staysUnbounded, named by the refusal (for instance "exact union or intersection of placed operands"), detail "no exact result". Only a deviation report pays for this, only for the booleans whose result is emitted (not the inner differences of a chain), and the search stops once the bound is within the requested budget. Release build, 1 mm budget: a 6 m wall with a 0.4 m round window in about 0.5 s, a 4 m I-beam with three round web holes in about 0.9 s.
Changed
- The certified branch and bound covers a piece with a flat region of the mesh (edge-connected, consistently wound, coplanar triangles) as well as with single triangles, so edges inside a planar face or a cylinder facet no longer have to be resolved to the bound's own size. Bounds are as sound as before and settle with less work.
0.3.9 - 2026-10-03
Added
ReferenceExactCompilercompiles differences of placed extrusions exactly (#228): a wall or slab under any rigid placement minus openings under theirs, perpendicular to its extrusion (doors, windows) or parallel (shafts), with rectangle, circle and line-and-arc profiles, through, blind, flush or touching its edges, and several openings per body as nested differences. It runs the general exact boolean (axiolid-brep-boolean, ADR 0080); two unplaced sharp rectangles along+zkeep the prism path. Unions and intersections of placed operands, operands that are not extrusions, a tool that is itself a boolean, scaled placements and configurations the general boolean refuses are refused by name; a difference that removes the whole subject isDegenerate. Faces that agree only up to rounding are read within the caller's tolerance; the result is then the exact boolean of operands moved by at most it, and a gap of ten tolerances is kept. One-segment semicircular arches compile (contour lowering splits them).ReferenceMeshCompiler::compile_mesh_with_deviationandDeviationReport(#232): next to the mesh, a certified upper bound on the distance from every point of the exact surface to the triangles, the paths that contributed (DeviationContribution,DeviationPath,DeviationBound::{Proven, Certified, Unbounded}) and whether the requested chord budget is met. #231's paths report the budget they are proven to; profiles report their flattening's bound (a derived profile its stretch); curved B-rep faces a per-triangle bound from the surface's second-derivative bounds plus each trim pcurve's lens; pipes along segments and arcs and primitive cylinders and cones the budget their constructions prove; disks swept along B-splines and ellipses a bound certified against the exact tube by branch and bound. Booleans, tapered extrusions, sectioned spines, bounded half-spaces, composites holding other curves and other frame laws are unbounded by name. An inherent method: theMeshCompilercontract is unchanged.
Changed
A swept disk's
fillet_radiusis honoured on polylines and composites of lines (IfcSweptDiskSolidPolygonal) instead of refused: each corner becomes a tangent arc of that radius (#232). A directrix with a corner and no fillet radius, which IFC leaves undefined and which used to be swept with sharp mitres, is now refused by name, as are a closed polyline directrix, a fillet that does not fit its segments, a disk radius at or above the fillet or bend radius, and a sweep range combined with a fillet radius (UnsupportedInput).A B-spline directrix with no corner knot reports its exact end tangents, so sweeps along it refine their stations and stand their end caps square to the curve, as along a conic (#232).
Curved B-rep faces are also refined where a triangle's certified bound misses the chord budget, by its widest free edge, up to a vertex cap; a pass that runs out of depth or vertices is discarded for the measurement-only refinement. Trim edges are sampled until their certified chord bound fits too, where that converges (#232).
Fixed
A swept disk along a line, a polyline, or a trim or composite of lines, polylines and circular arcs keeps every point of the exact tube within the chord budget of the mesh (#232). The directrix is read as exact segments and arcs and swept by
axiolid_construct::pipe(see its notes for the bound); it used to be sampled as a whole, each bend chorded for its centreline instead of the tube's outer side. At 1 mm: line + bend (R 0.1) + line, r 0.05, 1.184 -> 0.784 mm (502 -> 686 triangles); three bends out of plane 1.184 -> 0.803 mm (1652 -> 1882); at 0.1 mm R 0.04 / r 0.01 0.108 -> 0.077 mm, R 0.6 / r 0.3 0.115 -> 0.085 mm. A composite's sweep range is now cut at exact arc lengths rather than along its sampled chords. Lone circles and ellipses keep the #231 sweep, and composites holding other curves (ellipse arcs, B-splines) are swept as sampled.Trim samples earcut skips as collinear are put back on a curved face (#232): a straight pcurve sampled into many points came back as one long triangle edge, a T-junction against the face across it. The fan of thin triangles that putting them back leaves is then flipped to a Delaunay triangulation of the trim polygon, in parameters scaled to the surface.
0.3.8 - 2026-10-02
Added
exact_directrixandExactDirectrixare public (#230): they read a swept disk's directrix as one segment or one arc, exactly asReferenceExactCompilerdoes, so a consumer building exact boundaries that must match the compiled solids no longer duplicates the reading. Corners, other curve families and unbounded lines are refused by name.
Fixed
- Doubly curved meshes stay within the chord budget (#231). A revolution chorded its profile and its turn each to the whole budget, so the two deviations added inside a triangle: a torus (R 0.5, r 0.1) at 1 mm lay 1.46 mm from its mesh. A revolution, a tapered one included, now chords its profile to half the chord budget and its turn to the other half (see
axiolid_construct::revolvefor the proof), and its turn followsExecutionOptions::with_chord_errorinstead of the linear tolerance. Swept disks, fixed-reference and surface-curve sweeps along a circle or ellipse, plain or trimmed, give the section half the budget and refine the directrix until the section's far side fits the other half, and stand their end sections square to the curve rather than to its end chords (a disk r 0.1 along an arc R 0.2 lay 7 mm from its end caps at 1 mm). Every point of the exact surface now lies within the budget of the triangles, checked by dense sampling of tori (R/r down to 1.2), spheres, partial and skew-axis revolutions, a revolved rounded rectangle, a tapered revolution and sweeps along arcs with r/R up to 0.83, at 1 mm and 0.1 mm. A torus at 1 mm has 4928 triangles instead of 3520. A budget beyond 4096 steps round an axis is refused withBudgetExceededrather than met by a coarser mesh. Polyline, composite and B-spline directrices are swept as sampled, as before.
0.3.7 - 2026-10-02
Added
ReferenceExactCompilercompilesInstancenodes (#223): the source's exact B-rep is placed withExactBRep::transformed, so extrusions, revolutions and swept disks under rotated, reflected and translated placements (nested instances compose) stay exact. A scaled or sheared instance is refused by name, never approximated.ReferenceExactCompilercompilesSweptDiskexactly along one segment or one arc (#223): a bounded line, a two-point polyline, a circle or a sub-range of one, a trim of a line or circle (across the seam, as the mesh path reads it) and a one-segment composite. A directrix with corners, any other curve, an unbounded line and a disk reaching its arc's axis are refused by name.
0.3.6 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.5 - 2026-09-28
Fixed
- A boolean whose result touches itself is refused, not returned (#194): where operands meet tangentially -- a void tangent to its host's face -- the solid has no material between two faces, and the mesh boolean keeps two copies of the vertices there, closed by index but pinched by position. Consumers welding by position saw an edge with four faces. The result is now checked, positions welded, for an edge with more than two faces or a vertex with separate fans, and refused with
GeomError::Degeneratenaming the edge or point of contact. Circular voids tangent along an axis direction no longer pinch at all (construct). - Structural sections and rounded rectangles extrude to meshes (#193, via construct).
0.3.4 - 2026-09-27
Added
- Curve-bounded planes (#192), as IFC
IfcCurveBoundedPlanespace-boundary connection surfaces carry them: aSurfaceRelation::CurveBoundedover a planar basis compiles to a triangulated planar region withMeshClosure::Surface. Boundaries are read in the plane's parameters and mapped through its frame; the first is the outer loop, the rest holes. Straight boundaries (2D or 3D polylines, 3D ones on the parameter plane) are exact, curved ones chorded within the chord budget; the mesh faces along the plane's normal whatever the loop's winding. Crossing or degenerate loops, holes outside the outer loop and open boundaries are refused; a non-planar basis andimplicit_outergetUnsupportedInputnaming the capability.
Changed
- Every solid of a B-rep is tessellated, not only the first (#111): a composite profile whose members do not touch is several solids in one B-rep.
- A solid's void shells are tessellated (#120). They were dropped as "boolean intent", which silently filled every cavity of an authored
IfcFacetedBrepWithVoids-style B-rep and forced the exact booleans to refuse cavities. Each void is emitted facing into the cavity, so the mesh encloses the outer volume less every cavity; a void authored facing out of it (the STEP convention, reversed on use) is turned round, since a cavity can only remove material. A void shell that is not closed is refused rather than leaving the mesh open.
0.3.3 - 2026-09-25
Added
- The reference compiler honours
ExecutionOptions::with_chord_error(#165) everywhere it flattens a curve: profile arcs, circles and ellipses, sweep directrices, curved B-rep faces and edges, and CSG primitives. An instance scales the budget with its transform, like the tolerance, so it stays a world-space distance. Without a budget the chord error is the linear tolerance, exactly as before. Measured on a 5 mm disc extruded 1 m atTolerance::MILLIMETRE: 10 % short by default, 2.6 % at a 0.1 mm budget, 0.16 % at 10 um and 0.01 % at 1 um.
Fixed
- A surface model with a face whose outer bound encloses no area tessellates (#171): that face covers nothing, so it is skipped instead of refusing the whole model with
planar face bound has zero or non-finite area. Real Nova MEP exports write pipe-fitting end caps as bowtie quads through the pipe axis (an annulus with a negative inner radius), each with signed area exactly 0; 42 fittings, pumps and valves in two models were refused over them. A declared solid still refuses such a face, and a zero-area hole or a non-finite bound is still refused everywhere. - Closed authored meshes stay closed (#170). Planar faces of a
PolygonMeshand of a B-rep were triangulated with earcut, which drops corners on a straight run and runs diagonals and hole bridges over corners of the same face; the neighbouring face still split that edge at the corner, so the mesh cracked (T-junctions). Every edge earcut invents is now split at each face corner on it, within a band of a thousandth of the linear tolerance (1 um atTolerance::MILLIMETRE), so export noise on shared corners (1e-8 to 5e-8 m on real files) is judged the same on both sides. Authored ring edges are never split, and a thin triangle whose long side is authored is kept. Closure of an authored mesh is now read from its index connectivity instead ofaudit_mesh, which dropped real faces below its area threshold before counting edges. On two real ArchiCAD models this turns 505 authored-closedIfcPolygonalFaceSetproducts fromSurfaceintoSolid; no product that compiled before is refused.
0.3.2 - 2026-09-25
Fixed
- A directrix trimmed from a circle or ellipse ACROSS its seam sweeps the arc the trim names (#168). The trimmed curve runs from
startthe waysense_agreementsays, wrapping past the seam if it must; the directrix path used to sort the two trims and sample the complementary arc. Standalone that was silently wrong geometry (a315 -> 45degree bend swept the 270 degree arc); inside a composite the ends no longer met and the sweep was refused ascomposite directrix has a N unit gap. In a real Revit rebar model that was 1,494 bent bars, all writing a bend as270 -> 45,270 -> 15or270 -> 360rounded just past the seam. A full turn rounded past the seam stays a full turn. A sweepparameter_rangeon such a trim is read in the trim's unwrapped interval, so(330, 30)and(330, 390)degrees name the same sub-arc of a315 -> 45trim; an end off the arc is refused as before. Profiles already honoured this.
0.3.1 - 2026-09-24
Added
PolygonMeshfaces that are not plain triangles compile (#160): n-gons, concave faces and faces with holes (IFC4IfcIndexedPolygonalFaceWithVoids) are triangulated in their own plane, keeping the authored positions and winding. Plain triangles keep their exact corner order as before. A face whose corners leave its plane by more than the linear tolerance, that has no area, or whose rings cross is refused with an error naming its index.- B-reps with shells but no solid tessellate (#161): every shell is tessellated as authored and the result is reported as
MeshClosure::Surfacethroughcompile_mesh_reported, even when the shell is closed. Collections areSolidonly if every member is, and a boolean with a surface operand is refused. Authored meshes reportSolidexactly when they are closed, consistently wound two-manifolds.
Changed
- A
PolygonMeshwith non-triangular faces used to fail withGeomError::Unsupported; it now compiles. A B-rep with no solid and no shell is refused as "neither a solid nor a shell" instead of "has no solid".
0.3.0 - 2026-09-23
Fixed
InstanceandCollectionnodes no longer drop attribute channels and normals (#115). Both used to rebuild the mesh from positions and indices only, so a textured item lost itsuvchannel as soon as a product had a second item or was instanced — silently.Instancecarries channels through unchanged and now transforms normals by the inverse transpose instead of dropping them. Under a mirroring transform, corner-indexed channels and normals swap corners with the triangle.Collectionmerges channels by name. A channel on only some members becomes corner-indexed, with the other members' trianglesUNMAPPED; a channel on every member as per-vertex stays per-vertex. Members defining one name with a different width or blend drop it asDropReason::IncompatibleChannels.- Booleans keep the provider's channel fates, composed onto what each operand already went through, instead of discarding the evidence.
Added
ReferenceMeshCompilerimplementsMeshCompiler::compile_mesh_reported: the compiled mesh plus each channel's fate on its way to the root (worst over parallel members, sequential through booleans).
axiolid-mesh-compile-contract
0.3.6 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.5 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.4 - 2026-10-14
Added
PolylineCompiler::compile_polyline(#311): a curve node compiled to a 3D polyline, a separate operation rather than a second output kind ofcompile_mesh, whose signature stays as it is.PolylineOutcomecarries the vertices, whether the curve closes, and aChordDeviationreported the way a mesh's deviation is: the requested chord budget and a certified upper bound on the distance from every point of the exact curve to the polyline, withmeets_requested. The bound is never absent: a curve a compiler cannot bound is refused by name.- A direct dependency on
axiolid-coreforPoint3andScalar; it was already in this crate's closure throughaxiolid-mesh.
0.3.3 - 2026-10-09
Added
MeshClosure::OpenSolid(#265): the source declared a solid, but the compiled mesh is not closed (some edge is not shared by exactly two triangles running it in opposite directions). A compiler reports it instead of claimingSolidfor a mesh with boundary edges;CompileOutcome::solid_meshrefuses it by name.MeshClosureis#[non_exhaustive], so the new variant is additive.
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-24
Added
MeshClosureandCompileOutcome::closure(#161): whether a compiled mesh bounds a solid (Solid), is a surface model with area but no volume (Surface), or was not reported (Unknown, the default foruntrackedandtracked, so existing compilers build unchanged).CompileOutcome::solid_meshreturns the mesh only forSolid, so volume readers refuse a surface model instead of measuring a closed shell the source never declared a solid.with_closuresets it.
0.3.0 - 2026-09-23
Added
CompileOutcomeand the provided methodMeshCompiler::compile_mesh_reported(#115): a compiled mesh with the fate of each attribute channel. The default wrapscompile_meshand reportsattribute_fates: None("not tracked", not "nothing dropped"), so existing implementations compile unchanged.
axiolid-mesh-contracts
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-mesh-hidden-lines-contract
0.1.6 - 2026-11-14
Added
- Perspective fixtures for hidden lines (a box in front of the eye, the same box cut by the near plane, an eye inside a box, an exact non-dyadic boundary, an edge along the eye ray, an eye inside an edge-on triangle) and the refusal of a perspective near depth that is missing, zero, positive or not finite. A hidden-line provider that claims perspective is checked against them by
PerspectiveConformanceSuite::run, which reports a failure as the newPerspectiveConformanceFailure::HiddenLines. This replaces the 0.1.5 behaviour in which every claim failed closed withClaimUnverifiable; a visible-face claim still does until its fixtures land (ADR 0107, #387).
0.1.5 - 2026-11-13
Added
FaceGroups::PlanarandHiddenLineMesh::with_planar_groups: the groups are the edge-connected components of triangles joined across an edge of exactly two triangles with parallel, same-facing normals, decided exactly, each named by its smallest triangle (ADR 0106, #384). Conformance gains the planar isometric cube, a flat sheet and a fold.EdgeKind::GroupBoundaryandEdgeSelection::group_boundary(defaultfalse) withEdgeSelection::with_group_boundary: an edge between two triangles of different face groups that is neither a silhouette nor a selected crease. With the selection off the groups stay ignored and the output is unchanged (ADR 0106, #385). Conformance gains the planar isometric cube and a two-group square sheet.ViewProjection(Orthographic, the default, andPerspective), theView::projectionfield,View::perspective(frame, near)(refuses anearthat is not finite and below zero) and a defaultedsupports_projectiononMeshHiddenLinesandMeshVisibleFacesthat istrueforOrthographiconly. Registries route by it, so a provider that predates the field is never given a perspective view. The newconformance::perspective::PerspectiveConformanceSuiteinfers the claim fromsupports_projection: an unclaiming provider must refuse a perspective request by name, and a claim fails closed until perspective fixtures exist (ADR 0107, #387). No provider draws perspective yet.
Changed
OrthographicViewis renamedView, because a view can now be perspective. The old name stays as a deprecated type alias, so existing code compiles with a deprecation warning (ADR 0107, #387).
0.1.4 - 2026-11-07
Added
VisibleFaceEvidence::snap_displacement,rerouted_edges,collapsed_regionsandcollapsed_holes(#410, ADR 0100): how far snap rounding moved region boundaries, as aDisplacementBound, and how many edges it rerouted and regions and holes it collapsed.input_meshstarts them at zero. The conformance suite requires zero in every exact case.
Changed
MeshVisibleFacesregions are the exact regions snap-rounded to the binary64 grid (ADR 0100): a region whose area collapses under rounding is dropped and counted, never refused; rings are weakly simple; andVisibleRegion::triangleslists the triangles frontmost somewhere in an exact region it was snapped from. Where no edge is rerouted the vertices are still the exact points rounded once.
0.1.3 - 2026-11-03
Added
- The
MeshVisibleFacesoperation contract (ADR 0094, #383): visible face regions of meshes in the same orthographic view, withFaceGroups per triangle,FaceSource(group or near-plane cap),VisibleFaceRequest,VisibleFaceLimits,VisibleRegion, evidence andFACES_CAPABILITY_ID. conformance::faces::FaceConformanceSuiteforMeshVisibleFacesproviders: twelve exact cases (overlapping boxes, a hole under a box, an elevation, a cut column on a slab, a sheet that is never capped, interpenetrating sheets, a coplanar tie, an edge-on wall, two groups on one face, a crop, a far clip and a floating cluster), an area case for non-binary64 coordinates, the refusal cases, and determinism and orientation checks (#383).HiddenLineMesh::groupsandHiddenLineMesh::with_groups; the field is read byMeshVisibleFacesonly.
0.1.2 - 2026-11-03
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358). RefusedMesh: a refused input mesh and itsGeomError, as listed by the registry's audit so a caller learns every refusal in one call (#370).
0.1.1 - 2026-10-28
Added
- Conformance cases for near-plane caps (#138): a solid cut by the near plane hides what lies below its cap, a sheet is never capped, a shared diagonal needs the perturbation, a plane at the top of a solid cuts nothing, a cut solid with a face in the plane is drawn, and a fold under an equator edge needs the orientation of the perturbation.
0.1.0 - 2026-10-28
Added
- The
MeshHiddenLinesoperation contract (ADR 0090, #138): an orthographic view with optional near and far clipping, a 2D crop region, boundary, silhouette and crease edge selection, explicit limits, visible and hidden segments naming their source edge, evidence counts,HiddenLineError(aGeomErrorplus the mesh at fault), and a conformance suite with self-contained fixtures.
axiolid-mesh-section-contract
0.3.9 - 2026-11-13
Added
- Three conformance fixtures with a pinched cut boundary (piers meeting at a corner, three piers at one vertex, a block with a void touching its wall). A provider may answer them with the split cut or with
REFUSED_PINCHED_CUT_REGION(#373).
Changed
- The cut invariants document the pinch rule: rings are split where the boundary meets itself at a vertex, so each region is one connected piece written as simple rings that may touch (#373).
0.3.8 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.7 - 2026-11-03
Added
PlaneSectionEvidence::in_plane_crossingsandwith_in_plane_crossings: the distinct points, other than source vertices, where in-plane source edges cross (ADR 0093, #366).- Fixtures for stacked boxes, tubes and an L-step block, whose faces coincide in the plane, and for
Bothregions as cut regions.
Changed
- Every
InPlaneSide::Bothregion is a cut region, equal to it bit for bit (ADR 0093, #366). The region conformance suite checks it, and the registry checks it instead of refusing cut regions beside aBothregion. A provider that reports aBothregion and refuses only the cut request now fails the conformance suite; it must either refuse the plain request too or return the cut regions. in_plane_pairsandRegionLimits::max_in_plane_pairscount the candidate pairs examined for the in-plane faces (segment, piece-triangle and hole containment pairs of a merge), not only overlap tests. The count grows for the same mesh (68,104 to 105,600 on the 8,192-triangle bench grid), so amax_in_plane_pairssized to the old counts can now giveBudgetExceeded.- Documentation: the classes of the plane (
Below,Above,Both), that regions have pairwise disjoint interiors, and that a provider may still refuse overlapping in-plane faces by name.
0.3.6 - 2026-10-29
Changed
- Documentation of
CutRegion: the invariant that rings never cross, and the exactness section, now say that the decisions are made on exact source points (source vertices in the plane and exact crossings of source edges), and that the returned points are those computed in binary64, so rings that touch may cross by rounding error (ADR 0092, #377).
0.3.5 - 2026-10-29
Added
SectionRequest::with_cut_regions(CutRegionLimits)andPlaneSection::cut_regions: the cut of a solid, where its material lies on both sides of the plane, as closedCutRegions (outer ring, holes) whose rings run through the chain and region vertices bit for bit. RefusalsREFUSED_PINCHED_CUT_REGIONandREFUSED_CROSSING_CUT_RINGS; evidence fieldscut_regions,cut_region_verticesandcut_pairs. Additive (ADR 0092, #372).RegionConformanceSuiteruns every case again with cut regions requested, adds the L-step, wall at a sill, wall at a head, stepped slab, pocket floor and nested tubes fixtures, and reportscut_regions_claimed;RegionConformanceFailure::WrongCutRegions. A provider that answers the first solid case without cut regions predates them and is not claimed; one that returns them and later drops them fails.
Fixed
- The documentation said a closed chain runs counter-clockwise; a closed chain of a solid keeps the material on its left, so one around a hole runs clockwise.
0.3.4 - 2026-10-29
Added
SectionRole(Solid,Sheet) andSectionRequest::rolewithwith_role: a sheet is an open surface (terrain, a surface body), sectioned with boundary edges allowed.ChainEnd::MeshBoundaryand the evidence fieldsroleandboundary_edges(PlaneSectionEvidence::with_role). The default staysSolid, today's semantics. Additive (ADR 0091, #359).RegionConformanceSuiteadds the terrain cut by a vertical plane and through vertices, a sheet lying in the plane in both windings, an annulus sheet and a saddle (#359).RegionConformanceReport::sheets_claimedisfalsewhen the provider refuses the first sheet case withUnsupported; the sheet cases are then skipped.
0.3.3 - 2026-10-28
Added
SectionRequest,PlaneSectionand the providedMeshPlaneSectionmethodssection_requestandrequest_scratch_requirement: faces lying in the cutting plane come back asInPlaneRegions with anInPlaneSide, cut curves are transversalSectionChains (open ones end on a region), and touching edges are counted inPlaneSectionEvidence. A provider that does not implement the method refuses withUnsupported. The refusal names areREFUSED_OVERLAPPING_IN_PLANE_FACES,REFUSED_IN_PLANE_FACES_ON_BOTH_SIDESandREFUSED_SADDLE_VERTEX. Additive (ADR 0091, #359).conformance::RegionConformanceSuiteruns hand-derived fixtures for the method, among them a cube through its top face, a frame with a hole and a parapet with a post (#359).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-minkowski
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-model
0.3.19 - 2026-11-13
Changed
SurfaceRelation::RevolutionandLinearExtrusiondocument their parameterisation: a revolution isO + R_a(u) (c(v) - O)withuthe right-handed angle about the normalised axis andvthe swept curve's own parameter, an extrusion isc(u) + v dwithvin units of the direction's length (#411, ADR 0103).
0.3.18 - 2026-11-03
Added
SurfaceRelation::Ruled { first, second }andRuledSide: a surface ruled between two sides,p(u, v) = (1 - v) first(u) + v second(u). A side is an atomicCurve3node over an oriented interval, or an apex point. The builder refuses a non-finite or empty interval, a non-finite apex, two apexes and the same curve over the same interval, with the newGraphError::InvalidRuledSurface { detail }; a side that is no atomicCurve3is anInvalidReferenceType(#402).- Wire format 1.5: only a graph holding a
Ruledsurface is labelled 1.5; every other graph is written as before, and a 1.4 payload holding one is refused asUnknownKind(#402). - The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.17 - 2026-10-29
Changed
- The documentation of
SolidOperation::FixedReferenceSweepstates the frame law, the tangent-continuity requirement, howparameter_rangeis read, and what the reference compiler proves, reports unbounded and refuses (#122).
0.3.16 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.15 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.14 - 2026-10-16
Added
SolidOperation::BoundedHalfSpaceAlongZ(#321), a new variant withBoundedHalfSpace's fields: the kept side of the plane within the prism of the boundary along the placement'sz, infinite both ways and at any angle to the plane, the boundary atplacement * (u, v, 0)as written. This is how IFC'sIfcPolygonalBoundedHalfSpacebounds its half-space, by itsPosition.BoundedHalfSpacekeeps its fields and meaning. The builder validates the new variant's references as the old one's.SolidOperationis#[non_exhaustive], so the variant is additive.- Wire format 1.4 (ADR 0085): the variant lifts a payload to 1.4, and a 1.3 or older payload holding it is refused as
WireError::UnknownKind { kind: "BoundedHalfSpaceAlongZ" }. A graph without it is written byte for byte as before, labelled 1.0 to 1.3. Goldentests/wire/v1.4/every-kind.json.
Changed
BoundedHalfSpace.placementis documented as read whole (#322): itsxprojected into the plane fixes the rotation, and its scale, shear and handedness shape the boundary; only a degenerate frame is refused.
0.3.13 - 2026-10-13
Fixed
- JSON wire payloads (ADR 0085) now read every real back bit for bit, as the ADR promises.
serde_jsonwas used without itsfloat_roundtripfeature, whose fast parser lands one ulp off for about a quarter of ordinary doubles (12.838242182209141 read back as its lower neighbour), in every format version. CBOR was unaffected. A test now sweeps 6,000 doubles across twenty magnitudes through both encodings. The fix is in parsing only, so payloads already written are unchanged and now read back exactly.
0.3.12 - 2026-10-13
Added
- Run stations at their basis's start or end, measured by the kernel (#303, ADR 0082 amendment):
RunStationgainsbasis_end: Option<BasisEnd>(a new#[non_exhaustive]enum,StartorEnd), set by the constructorsRunStation::at_basis_start(offsets)andRunStation::at_basis_end(offsets);None, the default, is the statedstation.distanceas before. Such a station states the distance0, which is not read; a resolver reads the start as0and the end as the basis's length by the kernel's own measure. The graph refuses, asGraphError::InvalidStationwhen the node is pushed, a start station that is not the run's first, an end station that is not its last, and either stating a distance other than0; the stated distances still increase strictly, and whether the end comes after them is the resolver's check. - Wire format 1.3 (ADR 0085 note):
RunStation.basis_end, written only when it is set. A graph using it is labelled 1.3; one not using it is written exactly as before, as 1.0, 1.1 or 1.2. A 1.2 payload holding it is refused asUnknownFieldbasis_end, by the same test the writer labels by. Golden payloadtests/wire/v1.3/every-kind.json.
0.3.11 - 2026-10-12
Added
- Seam-snapping windows and seam sides on run stations (#301, ADR 0082 amendment):
SectionAtStationgainsseamandseam_window, set bywith_seam_sideandwith_seam_window, forSectionsAtStationsandOpenSectionsAtStations; a newRunStation(#[non_exhaustive], built byneworFrom<Station>and the same two methods) carries them in a new variant,CurveRelation::OffsetsAtStations, the general form ofOffsetByStations. Defaults areOutgoingand0, today's reading;Station,StationedSection,StationedOpenSectionand the #241 variants are unchanged. A negative or non-finite window on any run station is refused asGraphError::InvalidStationwhen the node is pushed. - Wire format 1.2 (ADR 0085 note): the two fields are written only when they are not
Outgoingand+0.0, andOffsetsAtStationsis a 1.2 variant. A graph using either is labelled 1.2; one using neither is written exactly as before, as 1.0 or 1.1. A 1.0 or 1.1 payload holding them is refused by name (UnknownFieldseamorseam_window,UnknownKindOffsetsAtStations), by the same test the writer labels by. Golden payloadtests/wire/v1.2/every-kind.json.
0.3.10 - 2026-10-10
Fixed
- The wire writer labels each payload with the lowest format version that represents it (#297, ADR 0085), not always with
FORMAT_VERSION: a graph with no seam-snapping window is written as"version": "1.0", byte for byte what 0.3.8 wrote, so a 0.3.8 reader reads it again; 0.3.9 labelled every payload1.1, which such a reader refuses. A graph holding a window anywhere (anOrientedCurveStationnode or the station of anInstanceAtStation, a window of-0.0included, since it is written) is written as1.1, as before. JSON and CBOR alike.
0.3.9 - 2026-10-10
Added
- A seam-snapping window on an oriented station (#294, ADR 0082 amendment):
OrientedCurveStation::seam_window, set bywith_seam_window, a non-negative distance in the basis's station measure (default0, the arc-length tolerance alone, so every existing station reads as before). A resolver reads a station within the window of one seam whose frame may jump on that seam, from itsSeamSide, and refuses a window holding two seams or a seam whose certified position straddles its edge. It also applies to the station of anInstanceAtStation. A negative or non-finite window is refused asGraphError::InvalidStationwhen the node is pushed. - Wire format 1.1 (ADR 0085): the window is written as
seam_windowand omitted at+0.0, so a graph without windows writes the 1.0 graph; an absent window reads as0, so every 1.0 payload still reads to the same graph. A payload claiming 1.0 that holds a window is refused asWireError::UnknownField.FORMAT_VERSIONis1.1; the 1.0 golden payloads stay and a 1.1 one is added.
0.3.8 - 2026-10-10
Added
- A versioned wire format for
GeometryGraphbehind an optionalserdefeature, off by default (#267, ADR 0085): JSON (serde_json) and CBOR (ciborium) encodings of one envelope,{"format": "axiolid-geometry-graph", "version": "1.0", "graph": {"nodes": [...], "roots": [...]}}, with node references as indices and enums externally tagged by their variant names.GeometryGraph::to_json,from_json,to_cborandfrom_cbor, and thewiremodule withFORMAT_NAME,FORMAT_VERSION,FormatVersionand the typedWireError. A reader checks the version before the graph and refuses a newer minor, another major, an unknown kind, variant or field, a non-finite number and malformed input by name; a decoded graph is rebuilt throughGeometryGraphBuilder::push, so it passes the same validation as any other. A writer refuses a graph holding a non-finite number. Golden payloads undertests/wire/pin format 1.0. - Both readers accept an integer where a real is expected when it is exactly a double (
i as f64 as i128 == i, so every|i| <= 2^53), as JavaScript and Python CBOR encoders write whole-number doubles as integers. A CBOR integer that is not exactly a double is refused asWireError::Malformednaming its path, wherever it stands. Integer zero has no sign and reads as+0.0. The CBOR reader unwraps the self-describe tag and refuses other tags, byte strings, simple values, non-text and repeated map keys. - With the
serdefeature, every node payload type derivesSerializeandDeserialize. ANodeIddeserialises only inside a graph payload.
0.3.7 - 2026-10-09
Changed
A
SolidOperation::BoundedHalfSpaceboundary may be aProfilenode as well as a 2D curve (#277, ADR 0084): a contour of lines and exact circular arcs states a boundary a polyline cannot. Graph validation accepts either and refuses anything else as"curve2 or profile"(a 3D curve was refused as"curve2"). No type, variant or field changed.The
stationmodule documents curve relations as station bases (#285): a composite, a trim and a curve placed at a station, nested, measured end to end in their pieces' common convention, each joint a seam. No API change.
0.3.6 - 2026-10-09
Added
Nodes placed at stations (#264, ADR 0082 amendment):
InstanceAtStation { source, station: OrientedCurveStation }(#[non_exhaustive], built byInstanceAtStation::new) and the node variantGeometryNode::InstanceAtStation, appended last. It reuses a curve, solid or surface in the frame of an oriented station, the station kept symbolic with its frame, orientation and seam side; the source's localx,y,zmap onto the oriented tangent, left lateral and up, its origin onto the station's point (a linear placement's reading), as thestationmodule documents. A placed curve is a 3D curve whatever its source (accepted where a curve or 3D curve is, refused in 2D slots), a placed solid or surface keeps its family, a placed profile is no longer a profile. The station is validated as an oriented station's is when the node is pushed; the node references its source, then the station's basis.Station seam sides (#263, ADR 0082 amendment):
OrientedCurveStationgainsseam: SeamSide(re-exported fromaxiolid-curve), the piece a station on a seam of its basis reads;OrientedCurveStation::newsetsSeamSide::Outgoing, the stated default and the evaluators' behaviour, andwith_seam_sidechanges it.CurveStationhas public fields, so it keeps reading the outgoing piece;CurveStation::with_seam_sidereturns the oriented station in its base frame reading the given side, andFrom<CurveStation> for OrientedCurveStationthe unturned outgoing one. Thestationmodule documents the seam rule and how runs of sections and offsets are mitred across a seam instead of taking a side.
0.3.5 - 2026-10-03
Added
Explicit station orientation (#246, ADR 0082 amendment):
StationOrientation { axis, ref_direction }, both optional and given as components in the station's base frame(tangent, lateral, up)-- the reading of a linear placement's axes relative to its curve -- with the axis the oriented up and the reference direction orthonormalised against it (Gram-Schmidt, axis primary);unit_axesfills the defaults(0, 0, 1)and(1, 0, 0).ORIENTATION_TOLERANCEbounds the sine below which the two count as parallel. Offsets stay in the base frame.GeometryNode::OrientedCurveStation(OrientedCurveStation): a curve station with an orientation.SectionAtStation(built bynew,with_tags,with_orientation; it is#[non_exhaustive], so fields can follow additively) and its two relations,SolidOperation::SectionsAtStations(closed profiles, the general form ofStationedSpine, matched by tag when tagged) andSurfaceRelation::OpenSectionsAtStations(open sections, the general form ofSectionedSurface). Both variants are appended to their enums.From<StationedSection>andFrom<StationedOpenSection>convert the #241 sections.GraphError::InvalidStationalso names a zero, non-finite or parallel orientation and a run of sections that mixes tagged and untagged ones.Stations (#241, ADR 0082):
Station(a distance andStationOffsetslateral/vertical/longitudinal) along a basis curve, measured in that curve's convention -- plan distance on an elevated or banked curve, arc length on any other -- with offsets in its section frame (StationFrame::Section, or the uprightStationFrame::Plan).GeometryNode::CurveStation(CurveStation)is a point and frame at a station;CurveRelation::OffsetByStationsa 3D curve through offsets at stations, interpolated linearly in distance;SolidOperation::StationedSpineclosed profiles standing at stations (StationedSection), matched by ring and vertex index;SurfaceRelation::SectionedSurfaceopen sections at stations (StationedOpenSection) joined by tag.GraphError::InvalidStationnames a malformed station when the node is pushed: a non-finite or negative distance, a non-finite offset, fewer than two stations in a run, distances that do not increase strictly, sections whose tags differ or repeat. A distance beyond the basis curve's length is refused when the station is resolved.
Changed
- Tags are matched as sets (#246): every section of a run carries the same tags, none repeated; an open section's tags may run in the first section's order or in reverse, so a
SectionedSurfacewhose sections are authored in opposite directions is now accepted and joined reversed. Any other order is still refused by name.
0.3.4 - 2026-10-03
Added
TrimSelector::ArcLength(Scalar)(#239): selects the basis parameter where the arc length along the basis, measured from its parameter0in the trim's sense, equals the value. Stored exactly; an evaluator resolves it by quadrature and a root find to a stated tolerance. It is a parameter-kind selector: it satisfiesTrimmingPreference::Parameter, a non-finite value is refused by validation, and equal arc lengths are an empty trim.
0.3.3 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.2 - 2026-09-27
Fixed
- A
SolidOperation::BoundedHalfSpaceboundary must be a 2D curve (#162). The graph accepted a 3D curve, which the compiler refuses, so such a graph validated and then could never compile; it is now refused when the graph is built.
0.3.1 - 2026-09-27
Added
A
Curve2::QuadraticGraphorCurve2::AngleGraphwith finite coefficients is accepted as a trim basis (#119).A
Curve2::Sinusoidis a valid trim basis when its three coefficients are finite (ADR 0071).
axiolid-numeric
0.1.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.1.3 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.1.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.1.1 - 2026-10-02
Added
SparseMatrix: compressed sparse rows assembled from triplets, with duplicates summed in input order so storage and products do not depend on triplet order.conjugate_gradient: Jacobi-preconditioned conjugate gradients for a sparse symmetric positive definite system (#129). Convergence is judged on the recomputed true residual, a spent budget is reported asStatus::BudgetExhaustedand refused byconverged(), and an asymmetric matrix, a non-positive diagonal or a direction of non-positive curvature is refused by name.
0.1.0 - 2026-10-02
Added
- New crate (#136): a shared numeric substrate with no dependencies.
find_root: Brent's method on a sign-change bracket, with a forced bisection whenever three steps fail to halve the bracket; returns the final bracket as a proven error bound.Polynomial::real_roots: every real root in an interval, isolated by derivative roots, with rigorous rounding bounds on each evaluation. Multiple roots and unseparable clusters come back asUnresolvedregions with the most roots they can hold.integrate: adaptive Gauss-Kronrod (G7/K15) with QUADPACK's error estimate and a panel budget;GaussLegendrefixed rules up to 512 points.Matrix,Lu,Choleskyand column-pivotedQr, with Hager-Higham condition estimates and forward error estimates;least_squaresandconstrained_least_squares(null-space method).levenberg_marquardtover aResidualstrait (analytic or forward-difference Jacobian), andminimize_scalar(Brent's localmin). Both report aStatus;converged()turns a missed tolerance into an error.- Non-finite input, singular, rank-deficient, asymmetric and indefinite matrices, and unbracketed roots are refused by name.
axiolid-nurbs
0.3.10 - 2026-11-13
Added
ExactCurveRefusal::Undecided: the roots along a traced section could not all be isolated with their multiplicity (a contact of order above eight, one rounding hides, or the work budget ran out). It says nothing of whether the curve meets the surface (#427).
Fixed
- A traced section against a surface that touches it at a higher order (a cylinder osculating the section, a plane tangent at its extreme) no longer hangs. Roots are certified with their multiplicity up to order 8, where the enclosures resolve it, by a Rolle chain on
det(grad F, grad H_j), the derivatives of the second field along the curve. Higher orders, contacts the enclosures cannot resolve (a flat stretch of a field next to crossings, often at orders 7 and 8 on trigonometric fields) and over-budget cases returnUndecidedinstead of a guessed touch at the depth floor (#427).
0.3.9 - 2026-11-07
Fixed
- A traced section against a surface that touches it reports the double root once, with multiplicity 2, instead of the root plus two spurious sign changes about 2.4e-8 either side: a stretch whose second derivative along the curve is enclosed away from zero is settled by a Rolle certificate before it is bisected (#397).
0.3.8 - 2026-11-03
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358). Derivation::TorusMeridianCircles: a plane containing a torus's axis meets it in the tube's two meridian circles, in closed form instead of a traced angle section (#382).
0.3.7 - 2026-10-28
Changed
- The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
Fixed
invert_surface_certifiedandinvert_periodic_surface_certifiedrefuse an off-surface point asOffSurfaceonce the certified lower bound exceeds the tolerance, where they returnedBudgetExceededfrom refining the retained boxes down to the parameter resolution (#320). Projection is unchanged.
0.3.6 - 2026-10-10
Added
exact_surface_intersectioncuts an elliptical cylinder by a plane in closed form (#287): the ellipse its conjugate semi-diameters span (a circle where they are exactly perpendicular and equal), or one or two rulings for a plane along the axis, cased exactly (Derivation::EllipticalCylinderPlaneSection,Derivation::EllipticalCylinderPlaneRulings). It was a ruled section.- Two cylinders, circular or elliptical, at least one elliptical, on exactly parallel axes meet in the rulings over the exactly isolated roots of a half-angle quartic (
Derivation::ParallelEllipticalCylinderRulings, #287). The pair was refused.
0.3.5 - 2026-10-02
Added
fit_curve3andfit_curve3_to_tolerance: least-squares B-spline curve approximation to scattered points (#150, ledger B11), distinct frominterpolate_curve3's exact pass-through. Uniform, chord-length and centripetal parameterisation; knot placement by the Piegl and Tiller averaging formula for approximation (eq. 9.68); optional equality- constrained endpoint interpolation and a second-difference smoothing term; optional Newton parameter-correction passes. Solves go throughaxiolid-numeric's QR-based least squares and equality-constrained least squares rather than normal equations. Every fit reports the achieved max and RMS deviation, computed by evaluating the result at the input points' own parameters, never estimated from the solver's algebraic residual. Refuses non-finite input, too few points for the requested degree or control count, coincident consecutive points under chord-length or centripetal parameterisation, invalid (zero) degree, and rank-deficient systems (reporting the numeric rank found and the rank required).fit_curve3_to_toleranceadds control points until the max deviation meets the caller's tolerance, refusing withGeomError::BudgetExceededrather than returning a fit that silently misses it.fit_surface_grid: separable tensor-product B-spline surface approximation to a rectangular grid of points (#150, ledger B11): fits each row alongusharing one design matrix, then each column of the result alongv, reusing the curve fit's solver and deviation reporting. Refuses ragged grids and the same input and rank failures as the curve fit, on either axis.
0.3.4 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.3 - 2026-09-28
Changed
- Traced sections decide signs of analytic series fields with certified arithmetic where
f64cannot (#181): exact zeros by a Laurent identity in the harmonics' common angle, point signs by fixed-point intervals at rising precision, and box signs by the Bernstein coefficients of a certified Taylor form. The tier answers only where the field is flat (along a line of contact or at a singular point), so ordinary sections trace exactly as before. Lines of contact that rounding hides over more than a twentieth of the window, refused asUndecideduntil now, are traced on analytic fields as on B-spline ones (ADR 0077).
0.3.2 - 2026-09-27
Added
- Surface knot removal, degree change and iso-curves (#141):
remove_surface_knot_u/_vremove a knot up to a requested number of times, keeping the surface within a tolerance, and report how many copies were removed with a deviation bound (SurfaceKnotRemoval). The bound comes from the control nets after inserting the knot back, so it holds everywhere, not only at samples; a knot that carries shape is left in place.elevate_surface_degree_u/_vraise a degree exactly.reduce_surface_degree_u/_vlower one within a tolerance (BoundedSurface), or refuse; rational surfaces are refused, as for curves.iso_curve_at_u/_vextract the exact iso-parameter curve. Rational surfaces are handled in homogeneous coordinates.
0.3.1 - 2026-09-27
Added
An exact tier for B-spline fields: signs at points and over boxes (and of derivatives) decided in dyadic Bernstein arithmetic, with no division, behind an interval filter, wherever
f64rounding cannot decide. A B-spline field's trace excludes pieces, finds sides monotone, places roots and tells lines of contact apart on exact signs, so it has no precision limit: lines of contact of every order its degree allows are found (tested tox^21), their derivatives tried up to that degree. Series fields keepf64, split pieces below rounding, and refuse lines of contact rounding hides over more than a twentieth of the window.Surfaces tangent along a whole curve (a line of contact) are traced. A first or second derivative of the field crosses zero regularly on the line and is traced instead; its curves are kept where the field vanishes along them with its gradient, as crossing or touching by the field's sign across. The field is then traced with tubes about the lines left out, and branches running into a line are bridged to it at a vertex. Crossing lines are sections; touching lines add none. Derivatives of every order are tried (up to 16), the tubes sized to where the field clears its rounding; only lines rounding hides over more than a twentieth of the window stay
Undecided.A trace trusts a value's sign only clear of its rounding, on a piece's sides and across it (it could certify cells along noise near high-order contact).
Contact to higher order than a saddle (a singular Hessian: a tacnode, where two branches touch each other) is a vertex too. The ends are checked against the field's sign changes about the point, and a tacnode's bridges arrive along the Hessian's null direction. Tangency along a whole curve without crossing is touching; with crossing it is
ExactIntersectionRefusal::Undecided, a new variant that also names an exhausted budget (both wereNotRegularCurve, which now means touching only).A B-spline section tangent to a sub-patch edge (a knot line, a window's or a split's edge) is traced: the touching crossing, where no proof can exist, is found by damped Newton and seeds the curve; a regular crossing that cannot be proven is still refused.
Faster traces near contact: singular points are sought as soon as a small piece is irregular, sides use the tighter of the direct and the mean-value bound and split their piece rather than search deep, and
SeriesField2::valueevaluates the value alone (bit for bit the same).Sections of two B-splines are certified. Every crossing of a sub-patch edge with the other sub-patch is isolated by Krawczyk's test on Bernstein enclosures of the surfaces and their partials, so every component is seeded; every chord of a traced curve is proven, by a parametric Krawczyk test over the chord's sweeping plane, to follow one arc (a chord that cannot be is halved, a curve that still cannot is re-traced with shorter steps, then refused). Patches are cut to the windows first, so a curve crossing a window inside one patch pair is found. B-spline curve/surface crossings use the same isolation.
spline_pair_intersection: every component of the section of two B-spline surfaces within parameter windows, asPairSection3curves. Bezier sub-patch pairs are split until their normal cones are apart, so no closed loop hides in one; sub-patch edge crossings seed every component, and each is followed on both surfaces to the window's edge, where its last node is solved exactly.exact_surface_intersectionreturns it for two B-splines (Derivation::PairTrace).exact_curve_surface_intersectionfor a B-spline curve against a B-spline surface: hull pruning over the Bezier pieces of both, then Newton.
Changed
- The trace separates the singular points it meets. An isolated point where the surfaces touch without crossing is dropped, with the boxes about it that rounding cannot decide. A trace that finds only such points answers
NotRegularCurve, as the closed forms do for touching. Branches crossing at a saddle meet at a vertex: the pieces about the crossing that cannot be certified are dropped, and each of the four branch ends is joined to the crossing by a bridge cell (the cubic matching the branch's value and slope at its end and its tangent at the crossing). Rounding there is measured on the field's terms at the point, not its coefficients. Whole-turn windows start an irrational fraction of a radian past-pi, so a symmetric section's special points never sit on the window's edge.
Added
section_curve_curve_intersection2(B8, #119): lines, conics, sinusoids, quadratic and angle graphs and implicit curves meet each other, in any pairing. One piece is traced over its span and the other's defining field has its roots isolated along it; roots on a graph's other branch are filtered out.section_curve_curve_intersection3(B8): space curves of the section families meet where one crosses a surface the other lies on, kept within a tolerance.extrema::minimum_distance(B7, #119): the smallest distance between points, curve spans and surface patches of any analytic or B-spline family and traced sections, bracketed and certified, with witness points. It is a branch and bound whose lower bounds are exact image boxes and projections on the joining direction, in mean-value form so they close quadratically at a closest point.Sections of B-spline surfaces by planes, quadrics and tori (#119, ADR 0077). The analytic equation is read on the spline's rational Bezier patches as Bernstein polynomials (knot insertion, then Bernstein products) and traced with the same certified subdivision, clipped to the spline's domain.
implicit_surface_intersectionandtrace_section_pcurvestake a B-spline carrier.exact_curve_surface_intersectiontakes a B-spline surface for lines, circles and ellipses. The curve is written as the meeting of two analytic surfaces; the first is traced on the spline and the second's roots are found along it.A plane through a cone's apex cuts rays along the rulings of the modelled nappe (
Derivation::ConeApexRulings, spans[0, inf)). It was refused before. A plane flatter than the cone meets it only at the apex (NotRegularCurve).Curve/surface intersection for the section families (B9, #119):
exact_curve_surface_intersectiontakes anImplicitSection, andsection_curve_surface_intersectiontakes any ruled, torus or traced section over a span. Roots are isolated along the curve's cells with interval bounds. Parameters areExactCurveParameter::Certified.trace_section_pcurves,extract_stretchandimplicit_viewgive a section's implicit pcurve on any analytic face, cut out between given points, and a ruled or torus section as an implicit curve.Traced sections (#119, ADR 0077):
exact_surface_intersectionnow builds a torus against a cylinder, elliptical cylinder, cone or torus off its axis (Derivation::ImplicitTrace). The section is found in the torus's parameters as every component of the other surface's equation, by certified subdivision into monotone cells. There is no marching, so no loop is missed for want of a small step. A singular point (surfaces touching where branches cross) is refused asNotRegularCurve.implicit_surface_intersectionexposes it with an explicit window for any analytic pair, andsection_field_ofthe field itself.exact_surface_intersectionderives ruled quadric sections (#119, ADR 0076): a cylinder or elliptical cylinder against a plane, sphere, cylinder, elliptical cylinder or cone, and a cone against a plane, sphere or cone (on the modelled nappes, decided exactly), when no line, circle or ellipse applies -- pipe tees, off-axis sphere/cylinder junctions, oblique cone cuts, parabolas and hyperbolas. Which spans of angle carry the curve is decided by exact root isolation of the discriminant.ExactIntersectionCurvegainsspansandDerivationgainsRuledQuadricSection.exact_surface_intersectionderives a ring torus's section by a plane or sphere off its axis (#119, ADR 0076) -- a pipe bend meeting a wall, a ball against a ring -- asu(v)solvingA(v) cos u + B(v) sin u = C(v), with spans and wrap points decided exactly (Derivation::TorusAngleSection).Exact intersection of analytic curves (#119):
exact_curve_surface_intersection(line, circle or ellipse against plane, cylinder, elliptical cylinder, cone, sphere or torus) andexact_curve_curve_intersection2/3(lines, circles, ellipses). The equations become integer polynomials in the line parameter or the conic's half-angle parameter, solved withaxiolid-exact: each hit, its multiplicity (2 = touching) and "the curve lies on the surface" are exact decisions, and a hit at a conic's half-angle singularity (theta = pi) is reported asAntipode. A cone counts only its modelled nappe; a line on the cone through its apex is refused asPartialOverlap. Points are rounded tof64for output only. B-spline operands stay on the certified tier.
Fixed
exact_surface_intersectiondecides tangent, parallel and perpendicular cases exactly (#119). Inf64a plane exactly tangent to a sphere along a normal like (3, 2, 6) came out as a circle of radius about 1e-7, an exactly perpendicular oblique plane cut a cylinder in a near-circular ellipse, and an exactly parallel one produced a huge ellipse instead of rulings. Only output coordinates are rounded now.
axiolid-overlay
0.4.1 - 2026-11-14
Added
free_faces,free_faces_within,FreeFaces(withlocate),FreeSpaceInput,FreeFace,FreeRing,Bridge,PieceSource,FaceLocation,FreeFaceEvidence,FreeFacesErrorandBarrierDefect(#490, ADR 0109): the bounded faces of the plane that obstacle polygons and open-segment barriers leave free, with the inputs each ring edge lies on. Gaps narrower than a closing width are closed by reported bridges, never by moving an input; a free region narrower than the width is a closed gap, returned apart. Rings are snap-rounded and certified as the visible faces are. Depends onaxiolid-exact-arrangementnow.
0.4.0 - 2026-11-13
Changed
- Breaking:
OverlayErroris#[non_exhaustive]and gainsHolesOverlapandPolygonsOverlap, whichRegion::newreturns; amatchon it needs a wildcard arm.HoleOutsideOuternow also names a hole that crosses its outer ring.Region::translatechecks its result asRegion::newdoes, since rounding each vertex can turn a contact into a crossing;Region::sweepmoves its copy unchecked, since the copy only feedsunion, and returns what it did before (#448, ADR 0104).
Fixed
MorphologyMode::Fastfills a needle notch deeper than the disc. A wall of needles 0.25 m deep under a disc of radius 0.4 m kept every notch, since no chord was proposed over a notch deeper thant: #340's room with a column and 124 such needles gave 562 corners, 500 needles 2,066, both asExact. Proposals now also passtover a needle no wider thanneedle_width(t, budget) = 2 sqrt(2 t budget + budget^2)whose vertices project inside its mouth, and across a vertex exactly on the chord. The bound of a notch deeper thantis the widest of its pieces between the vertices exactly on the chord, one needle's (2.5e-8 for a 2 cm fixture) instead of the whole wall's (2.5e-4), and a deep chord over the budget is split at its on-line vertex nearest the middle. The 124 needles give 68 corners for a dilation and 72 for an erosion, and the 2,002 and 10,002 corner walls ofbenches/disc_erosion.rs(deep) in 0.014 to 0.017 s and 0.07 s, where main takes 4.4 to 5.0 s at 2,002 corners. The side, the stated deviation andExactare unchanged, and the 30 cases of #330, #340 and #345 in that bench give the same results bit for bit. A wall whose valleys are collinear only up to rounding keeps them (#483).src/fill.rs,src/cover.rs, ADR 0087 (#428);scripts/probe_fast_morphology_mutants.pylists the faults its tests must catch.Region::newrefuses a polygon set whose rings cross into each other: part of a hole outside its own outer ring (HoleOutsideOuter), two holes of one polygon that cross, nest or repeat (HolesOverlap), and a point in two polygons (PolygonsOverlap). It accepted them, and the operations read such a set three ways: the fast morphology's notch fill took a hole's needles beyond its outer ring for the set's boundary. On #345's fuzz seed 21351 (needles at y = 2.2955 past the outer ring's top edge at 2.1202)dilate_outer_with(0.9046, fast(1e-2))left out (2.0172, 3.0256), 0.8993 from the region, with a stated deviation of 4.7e-3. The check is exact, with no tolerance, on one arrangement of all rings. Rings may still touch at points and share a stretch where their insides lie on opposite sides, which settled outputs do. A region of one ring costs what it did.Region::newof a floor of 100,000 corners with 200 holes takes 0.34 s instead of 0.083 s (10,000 corners: 0.032 s instead of 0.007 s;benches/ring_validation.rs, caseholes).overlayandunion_soupoperands are unchanged.src/nesting.rs;scripts/probe_region_nesting_mutants.pylists the faults its tests must catch (#448, ADR 0104).The exact arrangement finds the carriers of a piece among the edges that cover it, not among every edge sharing some stretch with its edge. An edge that many others stand on cost the square of their number: 1,000 rooms standing on one corridor edge took 2.2 s, 2,000 took 8.6 s and 4,000 took 35 s in
Region::new, and a rectangle with 1,000 holes standing on its bottom edge took 5.3 s, 2,000 took 19.7 s and 4,000 took 87 s. They now take 0.05 s and 0.22 s (corridor, 1,000 and 4,000) and 0.05 s and 0.23 s (holes). The arrangement is unchanged bit for bit (benches/ring_validation.rs, casescorridorandbays, #448).MorphologyMode::Fastfills the walls of a floor with sub-budget teeth in bounded time and memory. On #345's 3 m floor (teeth 0.5 mm deep, 1 mm budget) the chord over the room's corner, too deep for the budget, was split at the corner, and the chord left, from the corner along the wall, ended at a valley instead of a tip: not proved, it was split down to single edges, so thousands of teeth stayed and the sum kept their quadratic cost (an inner erosion by 1.4 m took 13 s and 0.86 GB at 10,000 corners, 71 s and 4.8 GB at 20,000, over 20 GB at 50,000). A chord not proved is now tried without its start edge, its end edge, or both, before it is split. The results are unchanged bit for bit, the excess is still within the budget and reported,Exactis unchanged. Release, median of 5, inner erosion, r = 1.4 m: 0.078 s and 6 MB at 10,000 corners, 0.39 s and 15 MB at 50,000, 0.77 s and 25 MB at 100,000, 1.57 s and 48 MB at 200,000, with the same for 1.49 m and 1.6 m (tools/benchmark/benches/disc_erosion.rs, wallsfloor;docs/research/fast-morphology-on-toothed-floors.md;src/fill.rs, #345, ADR 0087).scripts/probe_fast_morphology_mutants.pylists the faults its tests must catch.
0.3.22 - 2026-11-03
Added
check_arc_polygon_withandArcCheckOptions::with_clearanceadd an opt-in clearance check: a pair of edges whose distance has a local minimum strictly between zero and the given one away from their meeting points, or that form an exact cusp (they meet and leave in the same direction), is anArcPolygonDefect::Clearance, naming both edges and a certified lower bound on their distance; the neighbourhood of a meeting point at a nonzero angle (a narrow wedge) is not reported. A certified box bound drops far pairs, then exact signs over offset circles and segment distances decide the rest; a pair the exact tier cannot decide is reported withundecidedset, never cleared. A non-finite or negative distance is anArcPolygonDefect::InvalidClearance.check_arc_polygonand the default options are unchanged (src/arc_clearance.rs, #388, ADR 0095).scripts/probe_arc_clearance_mutants.pylists the faults its tests must catch.OverlayErrorimplementsDisplayandstd::error::Error, so it formats without{:?}and converts intoBox<dyn Error>(#354).
Changed
OverlayRefusal's message uses the wrappedOverlayError's own wording, andError::sourcereturns it (#354).
Fixed
MorphologyMode::Fastfills the notches between needles. Its chord proposals reached onlyt / 16from the chord's line (0.0125 m for a disc of radius 0.4 m,t0.2 m), so a wall of needles 0.05 m deep kept every notch and the sum kept its quadratic cost. Proposals now reacht. A notch whose spanned vertices all project onto its chord, of lengthg, also has the excesssqrt(t^2 + g^2 / 4) - t, which joins the least of its depth and its gap bound; proved insrc/fill.rs. Fast still keeps a needle deeper thant: nothing proposes a chord over it. The deviation and side promises are unchanged (src/fill.rs, #340, ADR 0087). Results on #330's toothed rooms are bit for bit the same; hole dilations take longer than before (see the research note).scripts/probe_fast_morphology_mutants.pylists the faults its tests must catch.
0.3.21 - 2026-11-03
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358). check_arc_polygondecides exactly whether an arc polygon's rings are simple and its holes nested: self-contact (crossings, pinches, tangencies, fold-backs), shared boundary, holes outside the outer ring or overlapping, contacts that cut the interior apart, plus the isolated contact points between rings (#352, ADR 0095).
0.3.20 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.19 - 2026-10-20
Added
overlay_withinandunion_soup_withintakeOverlayOptionsand the caller'sExecutionOptions(#338, ADR 0088): the tolerance is the options', and withExecutionOptions::with_memory_budgetthe subdivision charges what it is about to hold before allocating it and is refused withOverlayRefusal::BudgetExceeded { needed, budget }as soon as the charges pass the budget.OverlayRefusal::Invalidcarries every refusaloverlay_withandunion_soup_withmake. Without a budget nothing new is refused, and an admitted call gives the unbudgeted result bit for bit.tests/grown_soup.rsmeasures with a counting allocator that a call never holds more heap than its budget, admitted or refused, and that four times the unbudgeted high-water mark is admitted. The crate now depends onaxiolid-contracts.
Changed
- Ring validation no longer compares every pair of a ring's edges (#341). A ring of more than 32 edges asks the crate's box tree for the pairs whose boxes, grown by the tolerance, overlap, and the unchanged exact
segments_intersectdecides each, in the old order: the refusals, their order and the first pair found are as before, which a test checks against the all-pairs search on rings valid, crossing, touching and within the tolerance. And aRegioncarries the proof that its rings are valid under the tolerance it was built with:Region::newsets it, and a boolean on regions and the disc morphology set it from their settled output, soRegion::union,intersectionanddifferenceunder the same tolerance validate no ring again (holes are still checked against their outer rings). A region translated, or used under another tolerance, is validated as before; the proof is not part ofRegionequality. Measured withcargo bench -p axiolid-overlay --bench ring_validation(release), on a 40 m by 30 m floor with toothed walls:Region::newat 10,000, 50,000 and 100,000 corners took 0.17 s, 4.2 s and 17 s and now takes 0.006 s, 0.028 s and 0.059 s; aRegion::differencewith a 1 m square took 0.20 s, 4.4 s and 18 s per call and now takes 0.029 s, 0.18 s and 0.38 s.scripts/probe_ring_validation_mutants.pylists the faults the tests must catch.
Fixed
A soup of grown triangles whose union is one polygon no longer needs gigabytes (#338). A consumer's soup -- the strips of a faceted bar, each corner grown by a circumscribed 16-gon, hulled and fanned -- took 4.1 GB and 26 s for
union_soupat 128 facets and 1.9 GB and 10 s foroverlay(Intersection), and at 256 facets, and for the soup's union with itself at 128, failed to allocate. Three causes, each fixed with results unchanged:- The chain reduction walked the whole soup once and gave up on every ring when one cycle crossed itself: two overlapping hulls sharing vertices let the walk pass from one into the other. It now reduces such a soup group by group, the rings joined by edges given each way, so every fan becomes its hull's outline.
- Edges with the same two ends (hulls' flat sides lined up on one line and split there by the snap) were paired with one another for every copy. Only the first copy keeps their pieces, so the later copies are no longer paired among themselves.
- Where many edges meet at one vertex, every pair recorded the vertex for both edges. A point at an edge's own end is no longer recorded for that edge: it is its first or last stop, and the rings met there are never read.
Measured with
cargo bench -p axiolid-overlay --bench grown_soup(release, peak resident memory): circumscribedunion_soupat 64, 128 and 256 facets takes 0.08 s, 0.32 s and 1.6 s at 22 MB, 64 MB and 245 MB, and the intersection 0.09 s, 0.37 s and 1.8 s at 24 MB, 70 MB and 254 MB, against 0.07 s, 0.32 s and 0.94 s at 20 MB, 58 MB and 217 MB for the inscribed soup. The self-union at 256 facets takes 5.5 s at 482 MB. Every output that completed before is the same bit for bit, pinned by hash intests/grown_soup.rs.scripts/probe_grown_soup_mutants.pylists the faults the tests must catch.
0.3.18 - 2026-10-19
Added
A fast mode for the disc morphology (#330, ADR 0087).
Region::erode_inner_with,erode_outer_with,dilate_inner_withanddilate_outer_withtake aMorphologyMode:MorphologyMode::Exact, the default, is whaterode_innerand its siblings give, unchanged bit for bit. A CAD kernel takes it.MorphologyMode::fast(budget)first fills the summed set's notches with chords proved to lie outside it (src/fill.rs), so a wall of teeth becomes one edge. A chord is used only when the excess it adds stays withinbudget, in metres; a refused chord is split into shorter ones. The result keeps its inner or outer side. The excess is part ofMorphologyBound::deviation, andRegion::excessreports it on its own. A rule engine takes this mode.
The excess is a proved bound on how much larger the filled set makes the sum: about the square of the spacing of the corners on the chord over eight times the radius, and never more than the notch's depth. A budget that is not finite and nonnegative is refused with
OverlayError::InvalidOffsetDistance.Measured on #330's cases (#323's room with 0.5 mm teeth, as a room and as a hole, a 0.4 m disc, a 1 mm budget, release) with
cargo bench -p axiolid-benchmark --bench disc_erosion. At 10,002 corners, exact mode takes 3–11 s and about 1.2 GB, and grows about with the square of the corners (2.9–7.8 s and 1.15–1.19 GB in this run). Fast mode takes 0.010–0.013 s, 0.020–0.024 s and 0.036–0.045 s at 2,402, 5,002 and 10,002 corners, at 4–7 MB, so it grows about linearly. Its excess is 35 µm, 8 µm and 2 µm at those sizes, and 0.9 mm at 10,002 corners in the four cases where a chord within the budget cuts a corner of the room. Exact mode's hashes are those of 0.3.17 in every case. Tests check fast results pointwise against the true disc morphology on the certified side, within the stated deviation, for rooms and holes, inner and outer, erosion and dilation.scripts/probe_fast_morphology_mutants.pylists the faults they must catch.
0.3.17 - 2026-10-16
Fixed
- Minkowski sums and erosions, and the disc morphology built on them, no longer refuse with
OverlayError::ZeroAreafootprints that 0.3.15 handled (#331, a regression of #323 in 0.3.16). Where a wall runs parallel to a side of the convex piece -- a tessellated round body against the 64-gon ofdilate_outer, say -- with a corner an ulp off the chord across it, trimming an edge hull by the chord's hull left a sliver of three nearly collinear vertices, with an area under the tolerance squared, and the arrangement refused it. A chord whose hull or any trimmed hull would be such a ring is now not used: its hull and its trimmed hulls are dropped and the edges it spans keep their whole hulls. The consumer's footprints that failed now give 0.3.15's results bit for bit;tests/minkowski.rspins synthetic walls of that kind, 15 of whose 36 results 0.3.16 refused, against 0.3.15 by hash. Results 0.3.16 gave are unchanged, and so is the cost.scripts/probe_minkowski_degenerate_mutants.pylists the faults the tests must catch.
0.3.16 - 2026-10-16
Changed
- Minkowski sums and erosions, and the disc morphology built on them (
Region::erode_inner,erode_outer,dilate_inner,dilate_outer), no longer grow with the square of the corners along walls of many short edges (#323). On the 4.5 m by 5 m room whose long walls carry 0.5 mm teeth, eroded by a 0.75 m disc, 102, 602 and 1,202 corners took 0.045 s, 1.15 s and 4.4 s at 27 MB, 416 MB and 1.7 GB, and now take 9 ms, 68 ms and 0.23 s at 6 MB, 25 MB and 98 MB. With the corners exactly collinear they took 0.050 s, 1.4 s and 5.7 s and now take 2 ms, 8 ms and 12 ms, the rectangle's 2 ms plus reading the corners. Measured withcargo bench -p axiolid-benchmark --bench disc_erosion, interleaved runs against 0.3.15 on a shared machine. Each boundary edge gave one hull, each overlapping hundreds of its neighbours. Now a vertex exactly between its neighbours on one line (an exact orientation, no tolerance) is merged away before the hulls are built, and a chord between two vertices of a ring, proved exactly to lie in the summed set, gives one hull that covers nearly all of each edge hull it spans; those keep only what it does not hold, a sliver about the depth of the teeth. The union of the terms is the same set, so the results are too: bit for bit on the toothed room and on every sum and erosion of the crate's tests but two, whose results lose vertices lying exactly on the line between their neighbours, as the merged edges no longer split there.tests/minkowski.rspins the toothed room's erosions against 0.3.15 by hash, a unit test compares every route with the untrimmed hulls bit for bit, andscripts/probe_minkowski_cover_mutants.pylists the faults they must catch.
0.3.15 - 2026-10-14
Changed
- Exact straight-edge booleans of soups of overlapping slender parts, the plan footprint of an assembly, take time in proportion to their crossings, and an intersection with a small operand in proportion to the parts near it, with results bit for bit the same (#315). On
kslender boxes (1 m by 4 cm by 2 cm) over a square metre, the union of the soup with itself took 0.92 s, 6.3 s and 39 s atk= 100, 200 and 400 and now takes 0.23 s, 0.94 s and 3.9 s, growing as the crossings do (aboutk^2), and its intersection with a 5 cm square took 0.32 s, 2.1 s and 13 s and now 5 ms, 19 ms and 62 ms. Measured withcargo bench -p axiolid-benchmark --bench overlay_slender, interleaved runs against 0.3.14 on a shared machine. The time had grown aboutk^2.65, because the turned edges' boxes hold much of the soup and every piece of the subdivision searchedO(k)of them for its carriers and countedO(k)rings' windings. A piece's carriers are now searched only among the edges sharing two or more points with its edge, and across any stop only the rings met there are counted again, the others carried over; the points met along an edge are merged by one sort, and the crossings of a segment given more than once are computed once per pair and shared. An intersection leaves out, before the subdivision and after the snap, every ring whose box misses the other operand's: the module docs ofexact_overlaygive the argument that this changes no output. Debug builds check the carriers and the rings carried across a stop against the full search and count.tests/soup_snapshots.rspins the outputs on the consumer's soups, a torus, a cylinder, random triangles and an arc subdivision against 0.3.14 by hash, andscripts/probe_overlay_slender_mutants.pylists the faults it and the suite must catch.
Fixed
- The crate's internal exact booleans refuse a ring with a repeated vertex as
RepeatedVertexinstead of hanging on it (#309). Every public entry validates first, so no caller could reach it: a piece of no length had no point strictly inside, and the exact bisection for its sample never ended. The sample now refuses such a piece by name, and the straight-edge boolean checks for equal consecutive vertices before it starts. Tests call both with a ten-second limit, andscripts/probe_repeated_vertex_mutants.pylists the faults they must catch.
0.3.14 - 2026-10-13
Added
- An explicit opt-in to drop degenerate input rings instead of refusing the whole overlay, each drop reported (#256).
overlay_withandunion_soup_withtakeOverlayOptions; their default isoverlayandunion_soupexactly, a named refusal.OverlayOptions::new().dropping_degenerate_rings()leaves out a ring that has fewer than three points, an edge no longer than the tolerance (RepeatedVertex), or an area of at mosttolerance.linear()^2under the exactZeroArearule, and lists it inReportedOverlay::dropped/ReportedUnion::droppedas aDroppedRing: operand, polygon, outer or hole index,DropReason, and the magnitude of its signed area, summed exactly and rounded once, with a proven upper bound. A hole of a dropped outer is dropped and reported with it (OuterDropped); a degenerate hole of a kept outer is dropped alone. A self-intersecting ring is never dropped, nor aRepeatedVertexring that still crosses itself once its short edges are merged, since its signed area is not what leaving it out removes; non-finite points, holes outside a kept outer and invalid frames stay refusals too. The overlay of the kept rings is exact as usual, and its evidence counts the kept rings.scripts/probe_degenerate_drop_mutants.pylists the faultstests/degenerate_drop.rsmust catch.
Changed
- Exact straight-edge booleans are faster on soups whose rings share no edges, with results bit for bit the same (#218). The union of 2,000 random overlapping triangles takes about 94 ms, from about 290 ms (the grid backend: about 26 ms), and two crossing 1000-gons about 8.1 ms, from 12.3 ms, two thirds of it now operand validation; a union of two 60 x 60 meshes given as triangle soups takes about 19.5 ms, from 144 ms. Measured with
cargo bench -p axiolid-benchmark --bench overlay_soup, five interleaved rounds on a shared machine. The subdivision takes a segment no other boundary runs along as carried by its own ring only, and carries the rings containing a piece across a stop where exactly one other segment crosses, instead of searching for carriers and counting windings from a sample for every piece; any other stop counts afresh, and debug builds check both shortcuts against the search and the count. Samples on segments, crossings of two segments and input vertices keep their exact coefficients unbuilt until a question gets past their boxes; copies of a point share an identity that settles equality and interning at once; a ring's edges share their vertices, and each segment its direction. Rounding a crossing compares the offset from its guess in a tight interval before any exact midpoint, the subdivision's queries reuse their buffers, and the snap to the tolerance (#222) finds the vertices near an edge through a grid, not its whole band ofx, which made mesh soups slow. Arc booleans share the changes and are unchanged or faster inbenches/arc_overlay.tests/soup_membership.rschecks soups by point membership, andscripts/probe_overlay_soup_mutants.pylists the faults it and the suite must catch.
0.3.13 - 2026-10-10
Fixed
overlayaccepts its own settled output as an operand when that output holds two collinear edges that do not touch (#272). Operand validation asks whether non-adjacent edges meet, and its crossing test read the side of a rounded cross product with0.0counted as a side: two disjoint edges on one line whose cross products rounded to0.0and to a tiny positive residue counted as crossing, and the union of a turned U fed back was refused asSelfIntersection(6 of 20,000 turned copies in the report). Every contact decision is now exact, in the newcontactmodule that validation and settling share: a proper crossing needs each segment's ends strictly on both sides of the other's line, an end touches another edge when its cross product is within the tolerance and it lies in the edge's box widened by the tolerance, both decided exactly, and collinear edges therefore meet only when their extents do. The test whether a hole's corner lies inside its outer ring is an exact orientation sign too, and settling puts a proper crossing at the double nearest the exact one instead of at a point interpolated from rounded cross products, which edges crossing at a hair's angle put anywhere along the edge. A ring decides the same at any distance from the origin.scripts/probe_edge_contact_mutants.pylists the faults the tests must catch.
0.3.12 - 2026-10-10
Fixed
- Minkowski sums and erosions of large regions use memory linear in their subdivision, not quadratic in the region's corners (#292). Every piece of an
ArcArrangementkept a flag for every ring on each side, and the sum reads one hull per boundary edge, so a floor's walls of 3,400 corners and 58 rooms summed with a 0.9 m square needed more than 12 GB. A piece now keeps only the rings that hold it, as the subdivision computes them, and the sums and erosions read each face from that list: a polygon holds a side when its outer ring is listed and none of its holes, the union when any piece or translated copy does. Results are bit for bit the same. On a synthetic floor (cargo bench -p axiolid-benchmark --bench minkowski_plan, interleaved runs against 0.3.11 on a shared machine), 3,370 corners: sum 0.89 s and 531 MB peak before, 0.61 s and 124 MB after; with ribbed walls, 3,522 corners: sum 4.1 s and 2.2 GB before, 3.0 s and 469 MB after, and erosion of the rooms 4.7 s and 2.5 GB before, 3.5 s and 528 MB after. Time and memory now grow about linearly with the corners.ArcArrangement::regionsfills one buffer of flags per side instead of storing them.
0.3.11 - 2026-10-09
Fixed
- Small rings far from the origin keep their area and orientation (#274). A ring's orientation and its
ZeroAreacheck came from a shoelace over the coordinates as given, whose rounding at georeferenced positions (6e5, 5.6e6 m) swamps the area of a small ring: a 1e-4 m² triangle was refused asZeroArea, and a clockwise sliver of a chorded round corner read as counter-clockwise made the union of two triangles sharing an edge fail withSelfIntersection. Both are now exact signs of the area taken as a fan of triangles from the ring's first vertex, at every site that decides with them: validation (overlay,union_soup,Region::new, andvalidate_arc_ringfor a ring without arcs), the exact boolean's winding weights, canonical output orientation, settling, the outer-or-hole decision when linking result rings, and the orientationarc_overlayandArcArrangementgive their operands. A ring now decides the same at any distance from the origin.arc_ring_area,ring_area,polygon_areaandRegion::areasum from the first vertex too, so their rounding scales with the ring's extent. - A ring whose vertices all lie exactly on one line is a valid operand wherever it lies; a boolean leaves it out, as it encloses nothing (#219). Before, its rounded area decided whether validation refused it as
ZeroArea, so the plan shadow of a vertical face passed atx = 4.2and was refused atx = 4. A ring with arcs, or one passed tovalidate_arc_ring, is still refused, as the arc path has no rule leaving it out.
0.3.10 - 2026-10-02
Fixed
- Features closer than the tolerance touch (#222). Before the exact boolean, a vertex within
tolerance.linear()of an earlier vertex moves onto it, and one within it of another ring's edge is inserted into that edge. A door opening computed as3.9999999999999996..4.199999999999999between rooms ending at 4 and starting at 4.2 used to leave a gap one ulp wide, so the union came back in three pieces and coverage could not pass through the opening; the grid backend before 0.3.5 had closed it. Inputs with nothing that near another feature come back bit for bit as before.
Added
- Minkowski sums and erosions with a non-convex polygon (#145):
Region::minkowski_sumandRegion::minkowski_erosionnow take any simple polygon, where a non-convex one was refused withMinkowskiError::NotConvex. NewRegion::minkowski_sum_regionandRegion::minkowski_erosion_regiontake a whole region as the other operand -- non-convex, with holes, several components; the region sum is symmetric and cuts the operand with fewer vertices. A non-convex shape is cut into convex pieces of its own vertices (holes bridged in, ear clipping, then Hertel-Mehlhorn merging across diagonals while convex), every decision an exact orientation, and the cut is certified to tile the shape (the triangles' boundaries sum to the shape's); the pieces' sums with every boundary edge are united in the same exact arrangement as the convex case, so vertex sums are rounded once and output vertices once. Should no certified cut be found, the sum is taken from edge-pair parallelograms instead. Convex polygons take the path they took before, and give the same results bit for bit. A comb-shaped room of 152 vertices with a hole per two teeth is summed with a rectangle in about 30 ms, an L-shape (two pieces) in about 55 ms and a plus (three pieces) in about 180 ms; erosion takes about 1.3 times as long. The cost grows with the number of pieces times the region's edges. MinkowskiError::EmptyStructuring: an erosion by an empty region, under which every point would qualify.
Fixed
Region::minkowski_erosionby a polygon that does not hold the origin was cut to the region itself, so a translate of the polygon lying inside the region from a point outside it was missed (eroding[0, 1] x [0, 10]by[5, 5.5] x [0, 1]came out empty instead of[-5, -4.5] x [0, 9]). The erosion is now anchored on the region moved by a point of the polygon. Polygons holding the origin, and the disc erosions, are unchanged.
0.3.9 - 2026-09-30
Fixed
- A ring whose vertices all lie on one line -- the plan shadow of a vertical face -- is left out of a boolean, as it encloses nothing (#219). Its rounded area need not be zero, so validation let it through, and since 0.3.5 the exact subdivision refused it as self-intersecting; the grid backend before had answered as if it were absent. A union of such a ring with itself is empty again, and one with a polygon is that polygon.
Added
segment_intersectionsreports every intersection among many segments with a Bentley-Ottmann sweep (#146): each point where two or more segments meet, with every segment through it and where on it (start, end, interior, or a zero-length segment), and each collinear overlap with the segments covering it, inO((n + k) log n)time forkreported incidences. Every decision is exact (interval filter, then dyadic arithmetic), so shared endpoints, T-junctions, verticals, many segments through one point, overlaps and zero-length segments are handled rather than assumed away. Crossings areExactPoint2rationals, rounded once and correctly on request; input endpoints come back bit for bit. Non-finite input is refused with the segment's index. 64,000 short grid segments take about 0.2-0.4 s (cargo bench -p axiolid-overlay --bench segment_sweep).
0.3.8 - 2026-09-30
Changed
- Exact points share their coefficients, so copying one costs no arithmetic, and an input vertex builds its exact form only when a question about it gets past the interval filter, which most never do. Only the vertices of the result are rounded, not every vertex of the subdivision, and settling checks a hole against an outer ring's box before its edges. Results are unchanged; a soup of 2,000 overlapping triangles that share no edges takes about a fifth fewer instructions.
0.3.7 - 2026-09-30
Changed
- Straight-edge booleans on soups are much faster, with results bit for bit the same. Each operand's rings are first reduced as a chain: an edge given once each way between the same two exact points cancels, which changes no winding number, so a mesh given as a soup of its triangles shrinks to its outline before the exact subdivision. What is left is split into cycles through distinct vertices and used when every cycle is simple, decided exactly; otherwise the rings are kept as given. The union of two overlapping 60 x 60 triangle meshes (14,400 triangles) now takes about 10 ms, from about 1 s in 0.3.5 (the grid backend before it: about 2.5 ms); 160,000 triangles take about 85 ms. Also: the subdivision interns its vertices in a tree instead of a sorted vector, seeds a point's parameter on a segment in closed form, and settling finds repeated vertices and touching edges by hashing and a sweep instead of asking every pair. Soups of triangles that share no edges gain less (2,000 random triangles: about 370 ms, from about 430 ms).
0.3.6 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.5 - 2026-09-28
Changed
- Straight-edge booleans are exact (#173).
overlay,union_soupandRegionno longer go throughi_overlay's integer grid, which snapped every output coordinate, including untouched input vertices, to a step of about 1.5e-8 of the operands' extent. They now run on the exact subdivision the arc path uses: every ring of both operands cut at once, each piece classified by exact signs, kept by the operand's winding number under the fill rule. An input vertex the operation does not move comes back bit-identical (so[0,4]x[0,0.2]clipped by a box around it has area0.8, not0.800000011920929), and a crossing of two segments is the double nearest to the exact crossing. Fill rules and ring orientation keep their meaning (tested against the old backend). A vertex where the boundary runs straight on is dropped only when exactly straight. - Arc and arrangement output: rational vertices (every segment crossing) are now correctly rounded instead of rounded to about 50 bits.
- The exact subdivision indexes edges and rings in box trees, so building it over thousands of rings (a projected mesh's triangles) is no longer quadratic in the ring count.
0.3.4 - 2026-09-27
Added
minimum_enclosing_circle(#118): the least circle holding a point set, by Welzl's algorithm in a fixed visiting order. Every in/out decision is exact, so the support points are the exact minimum circle's; the centre is enclosed from exact dyadic values and the radius rounded up, so the returned circle holds every point, andCircleEvidence::errorbounds both the centre's offset and the radius's excess. Refuses empty and non-finite input (CircleError).
0.3.3 - 2026-09-27
Fixed
- Unions of many small cells are operands again (#191): a hole touching its outer ring at a vertex was called outside whenever that vertex was the hole's first and lay where the boundary-exclusive ray test counts it out, so the next operation refused the region with
HoleOutsideOuter. A hole is now outside only if a vertex of it lies strictly outside. union_soupsettles its output like every other operation, so its polygons are accepted byRegion::new(#191).
0.3.2 - 2026-09-27
Added
minimum_area_rectangle(#182): the least-area rectangle enclosing a point set, by rotating calipers over the exact convex hull. Caliper steps and the area comparison (W H / |d|^2, cross-multiplied) are exact; ties are broken by the least angle of the first axis, turned into[0, 90)degrees, so the result does not depend on input order. Returns anOrientedRectangle(centre, unit axes, half extents) andRectangleEvidence(hull size, count of tied orientations, and a bound on the rounding of the output -- for an axis-aligned rectangle the rounding actually done, measured exactly, so zero for a box with representable coordinates). Collinear input gives an exactly zero-width rectangle; empty or non-finite input is aRectangleError.Region::visibility_polygon(#184): the part of a region with holes in sight of a point inside it. An angular sweep round the viewpoint with every decision exact -- the order of vertex directions, which edges a wedge's middle ray meets and which is nearest -- so walls and holes cast exact shadows; only the shadow ends are rounded, once, and the result is presented like the other region operations. A viewpoint on the boundary or outside is refused (VisibilityError::NotInside).
Fixed
- Every region an operation returns is now one
Region::newaccepts and the next operation takes: unions, intersections, differences, morphology, Minkowski sums and visibility polygons of shapes that share collinear edges, touch at vertices or pinch holes against their outer rings could come back with edges shorter than the tolerance or rings touching themselves, and were then refused asRepeatedVertexorSelfIntersection. Outputs are settled by the same tests validation applies -- short edges merged, a vertex touching another part of its ring put on it, rings split where they pass a point twice, holes given back to their outer rings -- each move within the tolerance.
0.3.1 - 2026-09-27
Added
Region::minkowski_sumandRegion::minkowski_erosion(#145): the Minkowski sum and erosion of a straight-edged region -- non-convex, with holes -- by a convex polygon, at any rotation. Built in oneArcArrangementwith exact decisions: the region translated by a vertex of the polygon united with the convex hull of the polygon at both ends of every boundary edge, and erosion as the region minus the sum of its complement (within a box) with the reflected polygon. Vertex sums are rounded once, output vertices once. A non-convex structuring polygon is refused asMinkowskiError::NotConvex.Region::dilate_inner,dilate_outer,erode_inneranderode_outer(#163): disc morphology on a stated side of the exact result, by inscribed and circumscribed 64-gons moved a margin further out or in (past vertex and output rounding and the presentation's dropping of edges shorter than the tolerance).Region::boundgives the side (BoundSide::InnerorOuter) and the greatest deviation from the exact disc morphology (MorphologyBound). An inner erosion proves reachability of a route found in it; an outer erosion proves unreachability where none is.dilateanderodekeep their round joins, side unstated. A 200-edge region takes about a second.ArcArrangement(#120): the plane cut by several arc rings at once. Every crossing, shared boundary piece and ring membership is decided exactly (the same predicates asarc_overlay); crossing points are rounded once, into one vertex table. Each piece records the input edges it came from and which rings contain the region on either side, andregions(predicate)links the pieces bounding any membership set into outer rings and holes. Faces built from one arrangement therefore share vertices by index, which is what a stepped or stacked solid needs.
Changed
ArcArrangementskips rings whose box holds neither a piece's sample nor an edge: winding numbers and crossings are computed only near each ring. Many-ring arrangements are several times faster.arc_overlayis exact (ADR 0070, #155). Crossings, their order along each edge, inside/outside/on classification, linking into rings and hole ownership are all exact sign decisions on the given input, viaaxiolid-exact; none reads the tolerance. Crossing points of two curves are rounded tof64once, in the output, and edges shorter than the tolerance after rounding are merged. The public API is unchanged.cavalier_contoursis no longer a dependency. The arc path costs about 70 to 100 us per boolean on typical sections instead of about 1 us (benches/arc_overlay.rs).arc_overlayskips edge pairs whose padded bounding boxes are apart, and links pieces through a sorted index instead of a scan. Decisions are unchanged (still exact); cost now grows close to linearly with edge count: two overlapping 256-edge rings went from 779 ms to 7 ms, a 4096-edge outline against a small disc from 53 ms to 17 ms.
Fixed
overlayno longer rejects a U-shape or comb asSelfIntersection. The ring check treated an endpoint on the infinite line through another edge as touching it, so two collinear edges that share a line without meeting (the two ends of a U) were refused. A touching endpoint must now lie on the edge itself. Rings that genuinely touch are still refused.arc_overlayresults no longer depend on drawing units. The arc backend's thresholds are fixed in drawing units, so a 5 um gap survived a union drawn in millimetres but vanished in metres. The drawing is now scaled by a power of two so those thresholds sit at the caller's linear tolerance, capped so coordinates stay within what f64 resolves (ADR 0069). Superseded by the exact core above, which needs no scaling.
axiolid-pointcloud
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-pointcloud-reconstruction-contract
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-pointcloud-reconstruction-sdf
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-predicates
0.3.5 - 2026-11-07
Fixed
incircleandinspherereturned a wrong sign, certified as exact, for three classes of input, in every published release; each now returns the exact sign for every finite input (#430):- Coordinate differences of about 1e77 or more (
incircle) or 1e61 (insphere): the exact path's degree-4 and degree-5 products overflowed, and a point clearly inside or outside the circle was reported as a certain Zero ((s, 0),(0, s),(-s, 0)against(0, 0)and(2s, 2s)for s = 1e80, 1e100, 1e160). - Very small differences (s = 1e-100 and 1e-160 failed, 1e-80 did not): the products underflowed and the same Zero was reported.
- A NaN or infinite coordinate: it returned a certain sign; it now returns
Uncertain, asorient3ddoes.
The filters decline unless every nonzero coordinate difference has a magnitude in [2^-128, 2^128), so that no product or sub-product overflows or underflows (a gate on the permanent alone does not exclude a degree-2 sub-product that underflows and is then multiplied by a large lift). The expansion stage of the exact path runs only where every difference lies in [1e-60, 1e60] (
incircle) or [1e-45, 1e45] (insphere) and every nonzero rounding tail is at least as large as the lower bound; anything else goes to a fixed-size dyadic evaluation over the original coordinates (133 and 166 words, the accumulatororient2dandorient3dshare), which forms no difference and no rounded product. Cost, measured with the committedpredicatesbench on a loaded machine (ns per call, old and new binaries alternated five times, minimum to typical): random points 20.6 to 23 before, about 25 to 28 after (the difference gate costs about 20% on the filtered path); exactly cocircular lattice points (2^-10 grid, every call escalates) 1380 to 1460 before, 1420 to 1600 after; the same points scaled to about 1e80 1800 to 1870 before (the wrong Zero of the overflowing path), 6300 to 7000 after, now exact.- Coordinate differences of about 1e77 or more (
StaticFilter::orient3dcertified a wrong sign when the declared range was below about 1e-103, in every published release: the precomputed error bound underflowed to zero, so every nonzero rounded determinant was accepted (#429; 540 wrong signs in 3.2M fuzzed cases).StaticFilter::newnow returnsNonewhen a derived bound is below the normal range or not finite, the same refusal it already gave an overflowing bound. The module no longer claims that the static filter never certifies a sign the dynamic one would reject: it certifies only exact signs, and the dynamicorient2dfilter declines more (outside [1e-90, 1e90]) since #404.orient2dreturned a wrong sign, certified as exact, for four classes of input, in every published release; it now returns the exact sign for every finite input (#404):- Coordinate differences of about 1e154 or more: the products overflowed (a debug build panicked, a release build returned a wrong sign).
- Coordinates of very different magnitudes (a ratio of about 1e32 or more, e.g.
(1e-100, 1e-100),(1.5e-100, 1e-100),(-4.5e50, -1)gave Zero for a Negative triple): the expansion path lost error terms. - Differences near 1e-155: the filter's products underflowed and it proved a wrong sign (a Negative for a truly Positive triple).
- A NaN or infinite coordinate was reported as a certain Zero; it now returns
Uncertain, asorient3ddoes.
The exact path now evaluates the determinant over the original coordinates, as an exact sum of expansions of the coordinate differences and their rounding tails when those are in range (the usual exactly collinear case), otherwise in a fixed-size dyadic accumulator.
orient2d_filterdeclines differences outside [1e-90, 1e90] asUncertain. The cost is real for exactly collinear triples: per call, 24 ns on a full-mantissa millimetre grid whose differences are exact (12 ns before) and 50 ns on one crossing binades, where 45% of the triples have a rounding difference (12 ns before); measured with the committedpredicatesbench. Non-degenerate input is unaffected (about 15 ns).
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.4 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.3 - 2026-10-02
Added
in_diametral_sphereandin_diametral_sphere_filter(#126): is a point inside the smallest sphere through a triangle? For a point in the triangle's plane this is the coplanar in-circle test in 3D, which a 3D Delaunay triangulation needs for points in the plane of a hull face. A running-error filter, then exact expansions;Uncertainoutside the exactly evaluable range (non-zero coordinates beyond[2^-100, 2^100]).
Fixed
insphere_filterbounded its rounding error by the rounded 3x3 minors instead of by the absolute values of the elementary products (Shewchuk's permanent). When a minor cancelled, the bound was far too small, and the filter certified a non-zero sign for exactly cospherical points -- five lattice points of one sphere, four of them on a thin tetrahedron, were reported strictly outside (#126). The permanent is now Shewchuk's.
Changed
insphere's exact path first triesi128arithmetic when every coordinate difference is exact and all lie on one dyadic grid spanning fewer than 20 bits (integer lattices, for instance), and falls back to the expansions otherwise. Exactly cospherical lattice points reach the exact path on every call; this keeps it from allocating there.
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-27
Fixed
incircleandinsphere: the exact fallback rounded the coordinate differences tof64before its exact expansion arithmetic, so on nearly cocircular (cospherical) points whose differences do not fit anf64-- exactly where the filter hands over -- it could return the wrong sign. Delaunay flips driven by it cycled for ever (#190). The differences are now exact two-term expansions and every product after them is an expansion product; checked against an exact dyadic determinant.
axiolid-primitive
0.3.5 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.4 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.3 - 2026-10-10
Added
- An optional
serdefeature, off by default, derivingSerializeandDeserializeforPrimitiveandHalfSpace: the values the geometry graph's versioned wire format carries (#267, ADR 0085). Enums are externally tagged by their variant names and unknown fields are refused. No default-build change.
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-27
Added
Primitive::Torus(#142): a ring torus about local +z, by major and minor radius. Horn and spindle tori are not solids and are refused by the tessellator.Primitive::Wedge(#142): OCCT'sMakeWedgegeneral form with the height along local +z -- a base rectangle at z = 0 and a top rectangle, narrowed or shifted, at z = height. The top may collapse to a ridge or an apex.
axiolid-profile
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.2 - 2026-10-10
Added
- An optional
serdefeature, off by default, derivingSerializeandDeserializeforProfileand every value it holds: the values the geometry graph's versioned wire format carries (#267, ADR 0085). Enums are externally tagged by their variant names and unknown fields are refused. No default-build change.
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-project
0.4.0 - 2026-11-13
Changed
- Breaking: requires
axiolid-overlay0.4, whoseOverlayErroris#[non_exhaustive]with the newHolesOverlapandPolygonsOverlap, and whoseRegion::newrefuses rings that cross into each other.ProjectionError::Planarcarries thatOverlayError, so amatchon it needs a wildcard arm (#448, ADR 0104).
0.3.3 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-ray-mesh
0.4.2 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.4.1 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.4.0 - 2026-09-28
Changed
- Breaking: a candidate triangle index at or beyond the mesh's triangle count is refused with the new
RayMeshError::TriangleIndexOutOfRangeinstead of being skipped bynearest_hit_among; a broad phase built over a different mesh would otherwise report "no hit" for triangles it never tested.triangle_hitrefuses the same index instead of panicking in the mesh view. - The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-reference
0.3.18 - 2026-11-14
Added
bound::ChainPositionTableandbound::SeamSideare re-exported fromaxiolid-evaluate(#438): certified points, tangents and curvature vectors of an arc-length chain.bound::ElevatedPositionTabletakes a chain plan and reads its tangent and second derivative (jet3_on,jet_errors).
0.3.17 - 2026-11-13
Added
bound::ElevatedPositionTableis re-exported fromaxiolid-evaluate0.3.21 (#393): certified points of an elevated or banked curve over an intrinsic plan, whichaxiolid-mesh-compilereads for its swept disks.
Changed
- Requires
axiolid-evaluate0.3.21. section_requestwith cut regions no longer refuses a cut whose boundary meets itself at a vertex (#373, ADR 0092 amendment): the edges are paired there by exact angle and a ring that returns to a vertex is split, so corners that touch give two regions and a hole touching its outer ring is a hole that touches it. A ring that touches another at every corner is placed by the exact midpoint of one of its edges. Still refused by name: an edge in and an edge out along one ray or a chain the rings would part (REFUSED_PINCHED_CUT_REGION), edges that do not alternate or two of one direction along one ray (REFUSED_CROSSING_CUT_RINGS); aBothring that repeats a vertex stays refused.
0.3.16 - 2026-11-03
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
Changed
- Requires
axiolid-evaluate0.3.20, whose station frame laws bound their signed bend (#361). - The exact point store, the segment arrangement and the ring tracing of the section's in-plane faces moved into the new crate
axiolid-exact-arrangement, which the visible faces ofaxiolid-hidden-linesuse as well (ADR 0094, #383). The section's output and refusals are unchanged. A state that cannot occur in that code is nowGeomError::Degenerate, no longerBackendContractViolation. Wall clock ofcargo bench -p axiolid-reference --bench section_in_plane, best of nine runs on a loaded machine, before and after (ms per section): grid 51.9 and 54.1, footprint on a grid 51.4 and 51.0, skew grids 44.9 and 46.7, box on a slab 0.015 and 0.018. Single runs of one build spread from 53 to 192 ms, so this is unchanged within noise.
Fixed
triangulate_simpleandretriangulate_facerefuse a NaN or infinite coordinate withGeomError::InvalidInput.orient2dnow reports such a coordinate as uncertain instead of a certain zero (#404), and these entry points would otherwise panic on it.coplanar_overlapkeeps its signature and panics on a non-finite coordinate, documented under# Panics.
0.3.15 - 2026-11-03
Changed
ScalarSection::section_requestmerges the in-plane faces on an exact arrangement instead of refusing overlaps: faces of one side that overlap or only share a boundary segment form one region, and where faces of both sides coincide the region isInPlaneSide::Both. It no longer returnsREFUSED_OVERLAPPING_IN_PLANE_FACESorREFUSED_IN_PLANE_FACES_ON_BOTH_SIDES. EveryBothregion is a cut region when cut regions are requested; aBothring that visits a vertex twice is refused as pinched, and a region edge that bounds the cut with in-plane faces on the cut's side of it as crossing cut rings. It reportsin_plane_crossings, andin_plane_pairscounts segment, piece-triangle and hole containment pairs (ADR 0093, #366).- The request scratch bound is 2560 bytes per element (was 1536), and the crossings of in-plane edges are charged against the memory budget as they are created.
Added
- Bench
section_in_plane: the in-plane faces of a section request in wall clock.
0.3.14 - 2026-10-29
Fixed
- Cut rings are decided on exact source points: crossing, contact, containment and outer or hole no longer use the returned coordinates, which the projection to a turned frame and the edge interpolation round. Shells that touch in the source are no longer refused as crossing under a turned frame or an oblique section. An overlap that is in the input is still refused. The returned coordinates are unchanged (ADR 0092, #377).
Changed
- Depends on
axiolid-exactfor the exact points of the cut.
0.3.13 - 2026-10-29
Added
ScalarSection::section_requesttraces the cut of a solid as closed regions when asked (SectionRequest::with_cut_regions): region edges that the cut lies across are found with exact source signs, linked to the cut segments into rings, and nested exactly on the returned coordinates. Pinches and crossing rings are refused by name, contacts between rings are decided exactly (a ring crossing another through a vertex, a repeated vertex and a spike are refused); a sheet request is invalid input (ADR 0092, #372).
0.3.12 - 2026-10-29
Added
ScalarSection::section_requestsectionsSectionRole::Sheetmeshes: a chain ends on the mesh boundary at a crossed boundary edge or a boundary vertex (ChainEnd::MeshBoundary), a boundary edge in the plane next to an off-plane triangle is a touching edge, a sheet in the plane is a region, and a sheet folded flat onto itself is refused as in-plane faces of both sides (ADR 0091, #359). The request scratch bound is 1536 bytes per element.
0.3.11 - 2026-10-28
Added
ScalarSection::section_request: faces lying in the plane become regions with a side and holes, found by exact orientation predicates on the source coordinates; cut curves are transversal chains, open ones ending on a region, oriented and simplified exactly (no tolerance); touching edges are counted. Overlapping in-plane faces and saddle vertices are refused by name (ADR 0091, #359).
0.3.10 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.9 - 2026-10-18
Fixed
- Requires
axiolid-evaluate0.3.17, whose certified frame laws and intrinsic positions it re-exports andaxiolid-mesh-compile0.3.28 uses through it. 0.3.8 accepted any 0.3.12 or later, so a lockfile holding an olderaxiolid-evaluatedid not buildaxiolid-mesh-compile0.3.28.
0.3.8 - 2026-10-12
Fixed
clash::interferencecounted every narrow-phase pair of well-formed triangles that do not meet (TriangleTriangleRelation::Disjoint) as adegenerate_skips, through a catch-all arm, so a clash report over-stated how much of its input was untestable (two unit boxes 5 apart at a 10 m tolerance reported 120 of 144 pairs skipped). Disjoint pairs are now a verdict, and the match is exhaustive so a new relation cannot fall through again.
0.3.7 - 2026-10-04
Added
elevated:axiolid-evaluate's derivatives, certified chord and derivative bounds and grade corners of elevated and banked curves (#252), re-exported on the same footing ascurveandsurface.
0.3.6 - 2026-10-03
Added
station:axiolid-evaluate's station resolution (#241), re-exported on the same footing ascurveandsurface.
0.3.5 - 2026-10-03
Added
- Re-exports
axiolid_evaluate::arc_parameterandaxiolid_evaluate::chain(#239).
0.3.4 - 2026-10-03
Added
bound, re-exported fromaxiolid-evaluatelikecurveandsurface(#232).
Fixed
tessellate_primitiverefuses a cylinder or a cone whose chord budget needs more than 4096 segments withBudgetExceeded, as it already did for spheres and tori, instead of clamping silently to a coarser mesh (#232). Their bound is now documented: both are curved one way only, so the ring's sagitta is the whole distance from any point of the exact surface, caps included, to the mesh, and it gets the whole budget.
0.3.3 - 2026-10-02
Fixed
tessellate_primitivekeeps every point of a sphere and a torus within the chord budget of the mesh, not only its vertices (#231). Both directions of each used to get the whole budget, so their sagittas added inside a triangle (a 0.3 m sphere at 1 mm lay 1.99 mm from its mesh, a torus 1.90 mm); each now gets half, and the sphere's stack count is rounded up rather than down, which had let an odd segment count leave the polar step alone above its share. A budget that needs more than 4096 segments is refused withBudgetExceededinstead of clamped.
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-27
Added
tessellate_primitivemeshesPrimitive::TorusandPrimitive::Wedge(#142). The torus is a grid of planar trapezoids sized by the chord budget, round the axis for the outer equator and round the tube for the tube; horn and spindle tori, and non-positive or non-finite radii, are refused by name. The wedge's faces are planar and shared corners of a collapsed top are merged, so a ridge or apex wedge is still a closed, outward-wound solid; a reversed or non-finite top range is refused.
axiolid-refine
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.2 - 2026-10-02
Added
remesh::remesh(#149): isotropic remeshing towards a target edge lengthLin the Botsch–Kobbelt scheme CGAL'sisotropic_remeshingfollows. Each iteration splits edges longer than4/3 L, collapses edges shorter than4/5 Lwhere no edge longer than4/3 Lresults, flips edges to bring valences towards 6 (4 on a boundary), and moves free vertices towards the area-weighted centroid of their triangles in the tangent plane before projecting them onto the input. Works on aHalfedgeMesh; deterministic.- Boundary edges and, by default, edges sharper than 60 degrees (
RemeshOptions::feature_angle) are protected: never flipped, split on their own segment, collapsed only where their line runs straight, so feature corners stay bit-identical and the Euler characteristic, boundary loops and orientation are kept. Free vertices are projected only onto their own patch of the input (the pieces between protected edges), with a bounding-volume hierarchy per patch. - A collapse, flip or move that would turn a triangle's normal by a right angle or more, or leave it flatter than a height-to-longest-edge ratio of
1e-3, is skipped, never forced; so is a collapse or flip that would lower the smallest angle of the triangles it rewrites below 15 degrees. RemeshReportmeasures the result: edits made, the fraction of edges within[4/5 L, 4/3 L], mean valence deviation before and after, the smallest angle, and the largest distance of an output vertex from the input.RemeshErrorrefuses non-manifold, inconsistently wound, ragged or out-of-range input (wrappingHalfedgeBuildError), non-finite positions, degenerate input triangles, a non-positive target, a feature angle outside[0, pi]and an output over the triangle budget.fill_hole(#129): closes one hole of aHalfedgeMeshby Liepa's method: a triangulation of the boundary loop whose largest dihedral angle is the exact minimum (Liepa's and CGAL's recurrence only approximates it; the state here also names the triangle above each diagonal, atO(n^4)time), area breaking ties; then centroid refinement to the density of the surrounding edges with Delaunay edge flips, and biharmonic fairing of the new vertices. The patch is wound like its surroundings and shares the hole's edges. Refused by name, with the mesh unchanged: a halfedge that bounds no hole, a loop overmax_boundary_vertices, a loop with no area or one whose projection onto its mean plane is not simple, a hole every triangulation of which needs a degenerate triangle or an existing edge, a refinement overmax_new_vertices, and a fairing that does not converge.fair: places chosen vertices where the harmonic or biharmonic Laplacian vanishes with every other vertex fixed, with cotangent or uniform weights, solved by conjugate gradients fromaxiolid-numeric. Affine functions are reproduced, so a planar border keeps the faired vertices in its plane.subdivideandlimit_positions: Loop subdivision of triangle meshes and Catmull-Clark subdivision of polygon meshes, with cubic B-spline rules on boundaries, a face budget, and the limit position of every vertex from the schemes' limit masks.- New dependencies on
axiolid-numeric(the sparse solve) andaxiolid-predicates(exact orientation in the boundary-loop check).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.0 - 2026-09-23
Fixed
- A refinement that creates no vertex returns the input's channels and normals. It previously reported them
Preservedand returned a mesh without them.
axiolid-route
0.4.0 - 2026-11-13
Changed
- Breaking: requires
axiolid-overlay0.4, whoseOverlayErroris#[non_exhaustive]with the newHolesOverlapandPolygonsOverlap, and whoseRegion::newrefuses rings that cross into each other.skeletonandCostRegion::newtake overlay'sPolygon, so a caller builds it with overlay 0.4 (#448, ADR 0104).
0.3.9 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.8 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.7 - 2026-10-02
Fixed
- A weighted map counts the free side of a cost edge left just beyond a wall (#222). The exact overlay leaves a cost region cut to a turned wall with corners a rounding step beyond it, so no wall runs exactly along the edge between them, and a walk along that edge was counted at the cheaper side -- outside the free space -- at factor 1. Along an edge on or beyond a wall within the touching reach, only the region's side is now free, as along the wall itself. A factor-2 square on a wall turned by the 3-4-5 angle brackets 10.054..10.062 again, from 9.28..10.062.
0.3.6 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.5 - 2026-09-28
Added
- Seeded weighted maps (#198):
weighted_distance_map_seededandweighted_distance_map_seeded_withintake(point, weight)targets, each starting at its own non-negative cost, asdistance_map_weighteddoes for plain maps (#197); both bounds start there, andWeightedReach::costcounts the weight. With every weight zero the map and its answers areweighted_distance_map's exactly. weighted_forced_walkandweighted_forced_walk_within(#198): the cheapest walk that enters a polygon over two weighted maps with the same costs, bracketed with a witness asforced_walkis. The cell bound falls at twice the steepest factor meeting the cell, and the pair bound runs over both maps' vertices and cost-edge intervals with their lower bounds, at the cell's own factor when no cost edge meets it.WeightedForcedWalk::shortestbrackets the cheapest walk overall; the result is never narrower than the maps' brackets at the witness, and the search stops at the tolerance plus those.
Changed
- A cost region touching the region's boundary up to rounding is taken as touching it (#198), no longer refused with
MapError::CostCrossing: a cost vertex within 2^-24 of the region's extent (and a few ulps) of a region edge, on its free side, is moved just beyond it, and a cost edge may cross a region edge that near one of either edge's ends. An edge so left on or beyond a wall is wall-borne, as one exactly along it. So a footprint clipped to the free region, its corners rounded by the overlay, builds a map, and leaves no sliver along a wall costing 1.
Fixed
- Weighted maps on cost edges at an angle (#198). The points cutting such an edge are interpolated and lie off its line by rounding, which made three decisions go wrong: an interval's sides were taken of its rounded ends, so could both read the factor of one side; an edge could appear to cross every hop to its own intervals and block them -- both could put the lower bound above the distance; and every hop along the edge was charged the greatest factor above. Sides are now taken of the edge's exact ends, an edge never blocks hops from its own line, and a hop along an edge is costed above as the walk along the edge itself, which lies that close to it.
0.3.4 - 2026-09-28
Added
Weighted targets (#197):
distance_map_weightedanddistance_map_within_weightedtake(point, weight)targets, each starting at its own non-negative distance -- a stair landing carrying the rest of a walk beyond it.nearestanswers the least route length plus weight, in the newReach::distance, and names the target the route ends at, which may be a lighter target the heavier one is reached from;route.lengthis the route's own.farthest_pointandforced_walkkeep their brackets (the weighted distance is still 1-Lipschitz), andForcedWalk::shortestcounts the origins' weights. With every weight zero the map isdistance_map's.MapError::InvalidWeightrefuses a negative or non-finite weight.forced_walk(#196): the shortest walk from an origin to a target that enters a polygon,min over p of d_origin(p) + d_targets(p), bracketed to a tolerance from two distance maps over the same free space. Branch and bound as forfarthest_point, with a second lower bound from the maps' own vertices (D(u) + D'(v) + max(|u - v|, dist(T, u) + dist(T, v))over the vertices a cell may see), which is exact on every cell the walk runs straight through.ForcedWalk::shortestis the shortest walk overall; a lower end above it proves no shortest walk enters the polygon. A polygon no walk reaches is infinitely far, not an error.FarthestError::MismatchedMaps, for two maps over different region or barriers.Weighted distance maps (#195):
weighted_distance_maptakes cost regions (CostRegion: a polygon and a factor of at least 1; overlaps take the greatest, and a walk along a cost edge pays its cheaper free side) and points along cost edgesspacingapart.WeightedMap::nearestbrackets the weighted distance to the nearest target (WeightedReach::cost) and gives the walk whose cost is the upper end. The upper bound is the exact visibility graph with those points added, each edge costing its weighted length; the lower bound is a graph over vertices and intervals of cost edges whose hops cost no more than any piece of an optimal walk between them, with the states an optimal walk cannot take -- turning back at a cost edge, two pieces in a row along one line -- barred. Square crossings are exact; otherwise the gap is first order in the spacing.weighted_farthest_pointbrackets the farthest point of a subregion by the same branch and bound, each triangle's slope its greatest factor.MapError::InvalidFactor,InvalidSpacingandCostCrossing: cost regions may nest, touch, share edges and run along the region's boundary; one crossing an obstacle or another's edge, or running along a barrier, is refused.
Changed
- Segment crossing tests reject disjoint bounding boxes before any predicate, and a segment's pass-through check visits only vertices with obstacle rays:
distance_mapbuilds several times faster.
0.3.3 - 2026-09-27
Added
skeleton(#139): the corridors of a region with holes as a graph -- path ends, junctions and paths -- for circulation checks. Nodes are Voronoi vertices of the walls sampled at mostspacingapart, over a constrained Delaunay triangulation, pruned of spurs into corners as in the lambda-medial axis (prune; 1.5 drops spurs into right-angled corners). Every node is decided inside the region exactly and carries aclearanceinterval proven to contain its distance to the nearest wall; each path end names the wall it runs into (ahead). The skeleton's position approximates the medial axis and is not certified.
0.3.2 - 2026-09-27
Added
distance_mapanddistance_map_within(#186): shortest-path distances to the nearest of several targets, from one multi-source Dijkstra over the visibility graph.DistanceMap::nearestgives, for any point of the region, the nearest target's index and the route there (Reach). Refusals are typed:MapError::NoTargets,MapError::TargetOutside.farthest_pointandfarthest_point_within(#186): the greatest distance to the nearest target over a polygon subregion, as aLengthIntervalthat contains the true value, with a witness point (Farthest). Branch and bound over a constrained triangulation of the free space, bounding each cell by the 1-Lipschitz property of the distance from anchors proven inside it; widened for the rounding of lengths and midpoints. Part of the subregion that no target reaches is refused with the triangle as evidence (FarthestError::Unreachable); crossing barriers are refused (FarthestError::CrossingObstacles).
Fixed
- Routes no longer squeeze through a point where obstacles meet (#189): a barrier's foot on a wall, two holes touching at a corner, a barrier bent or joined at a vertex. Each graph vertex is split into the free sectors between the obstacle rays leaving it, ordered exactly; a route moves over (vertex, sector) states, each edge is taken on one side of travel and attached to the sector beside it at each end, and passes through a vertex only on a side with no obstacle ray and free space. Two polygons sharing an edge are walkable along it on either side.
shortest_pathandDistanceMapshare the graph.
0.3.1 - 2026-09-27
Fixed
- A route could run along one wall, through a vertex and on across a gap outside the region where two walls line up (#187): visibility tested only proper crossings and the segment's midpoint. A segment is now cut at every obstacle vertex lying on it, decided exactly, and each stretch is either along an obstacle edge or has its midpoint in the region. Two rooms whose corridor is cut are
DisconnectedComponentsagain.
axiolid-spatial
0.3.6 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.5 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.4 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.3 - 2026-09-30
Added
- Wachspress and discrete harmonic coordinates (#143, ledger row H5):
wachspress_coordinates2anddiscrete_harmonic_coordinates2, both for a strictly convex polygon (refused by name otherwise,BarycentricError::NotConvex), positive and smooth inside, boundary-linear on the edges, and refused asBarycentricError::Undefinedat a pole (a denominator vanishing on the line through a non-adjacent edge, or discrete harmonic's own collinear fan angle). Andmean_value_coordinates3(Floater, Kos and Reimers 2005), extending mean-value coordinates to a closed triangle mesh in space, one weight per vertex, exact at vertices and on a face's own plane inside its triangle.
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-27
Added
- Barycentric coordinates (#143):
triangle_barycentric2,triangle_barycentric3(for the point's projection onto the triangle's plane) andtetrahedron_barycentric, exact at corners; andmean_value_coordinates2for simple polygons, convex or not, which interpolate the boundary linearly and reproduce points inside. Shapes thinner than the linear tolerance, non-simple polygons and points where mean-value weights cancel are refused withBarycentricError.
axiolid-surface
0.3.6 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.5 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.4 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.3 - 2026-10-10
Added
- An optional
serdefeature, off by default, derivingSerializeandDeserializeforSurfaceand its elementary surfaces: the values the geometry graph's versioned wire format carries (#267, ADR 0085). Enums are externally tagged by their variant names and unknown fields are refused. No default-build change.
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-27
Changed
BSplineSurfaceis defined inaxiolid-curve(so a curve traced on a B-spline surface can carry its carrier, ADR 0077) and re-exported here unchanged: same fields, same derives. Requiresaxiolid-curve0.3.1.
axiolid-tessellation-contract
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The package metadata names rheonda UG (haftungsbeschränkt) as author, and commercial licences are available from rheonda UG (haftungsbeschränkt) (
LICENSE-COMMERCIAL, ADR 0089).
0.3.2 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-tetrahedralize
0.1.2 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.1.1 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.1.0 - 2026-10-02
Added
Delaunay3(#126): exact incremental 3D Delaunay tetrahedralization. Bowyer-Watson insertion in Hilbert-curve order, located by a stochastic visibility walk, with an infinite vertex for the convex hull.from_points,insert,tetrahedra(positively oriented, with adjacency),hull_triangles(outward),vertex_of,dimension.orient3dandinspheredecide every step exactly; cospherical points, points in the plane of a hull face (decided byin_diametral_sphere) and other ties are broken by a symbolic perturbation of the lifted points, so the result is unique for a point set whatever the insertion order. Exact duplicates are merged (Insertion::Duplicate). Refused by name (Delaunay3Error): fewer than three dimensions, non-finite coordinates, and non-zero coordinates outside[2^-100, 2^100].
axiolid-topology
0.3.4 - 2026-11-07
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358).
0.3.3 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.2 - 2026-10-10
Added
- An optional
serdefeature, off by default, derivingSerializeandDeserializeforBRep, its entities and its typed handles (as plain indices): the values the geometry graph's versioned wire format carries (#267, ADR 0085). Enums are externally tagged by their variant names and unknown fields are refused. No default-build change.
0.3.1 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
axiolid-triangulate
0.3.7 - 2026-11-03
Added
- The package now ships
LICENSEandLICENSE-COMMERCIAL(symlinks to the repository root), so the licence texts travel with the archive (#358). TriangulationError::NonFinitePoint { index }: a point with a NaN or infinite coordinate is refused up front (#355).
Fixed
- A point with a non-finite coordinate is refused with
NonFinitePoint. Recovery by walking the segment would read its orientation as exactly collinear and could return a made-up split (#355).
Changed
- Constraint recovery no longer scans every triangle per missing constraint. Edges are found through a vertex-to-triangle index that every flip keeps current, and the edges a constraint crosses and the vertices on it are found by walking the segment with the exact orientation predicate. The triangulation is identical, triangle for triangle. On a jittered grid with zigzag breaklines (
triangulate_constraintsbenchmark, release build), 10,000 points with 4,950 segments took 12.1 s and now take 0.065 s; 49,729 points with 12,432 segments took 168 s and now take 0.175 s (best of three, same machine) (#355).
0.3.6 - 2026-10-28
Added
triangulate_domain, withRingandRingKind: a constrained Delaunay triangulation of the area inside outer rings and outside holes, by an exact winding-number flood fill. Rings may share vertices and edges and be listed either way round; points outside the domain are dropped and listed by the newTriangulation::outside_points(#353).TriangulationErrorvariantsRingOutOfRange,TooFewRingVertices,RepeatedRingVertex,FoldedRing,MisnestedRingandEmptyDomain(#353).
Changed
refinehonours the domain of atriangulate_domainresult: it re-triangulates inside the rings instead of re-expanding to the hull (#353).
Fixed
- A constraint that passes exactly through input vertices, or ends on another constraint, is split at the vertices on it and recovered as the pieces between them, instead of being refused as
CrossingConstraints;Triangulation::constraintslists the pieces (#360).
0.3.5 - 2026-10-28
Changed
- The licence is now
MPL-2.0 OR LicenseRef-Commercial(wasMPL-2.0): a commercial licence is available from the copyright holder (LICENSE-COMMERCIAL, ADR 0089, #358). - The minimum supported Rust version is now 1.99 (was 1.88), and the workspace builds on Rust 1.99.1 (#349).
0.3.4 - 2026-10-12
Added
MergedPointandTriangulation::merged_points(#300): the input points merged into an exactly coincident one, each as its input index and the kept index that stands for it (the lowest input index at that position), in ascending input order.TriangulationError::CoincidentConstraint { a, b }(#300): a constraint whose two endpoints are distinct input points at exactly the same position. Merged, they are one vertex, and the constraint would be an edge from it to itself.
Fixed
triangulatemerges exactly coincident input points instead of splitting each into the mesh at its twin's position (#300), which left zero-area and overlapping triangles that used one directed edge twice -- in a 3x3 grid with its first row repeated, edge (3, 10) twice -- and reported nothing. A point found at a vertex already inserted is recorded as merged into it; the triangles andTriangulation::constraintsuse the kept indices,pointsstill holds every input point, andmerged_pointsreports the merge, so it is lossless. Duplicates in free points, outlines, holes and constraints triangulate validly; a constraint between two coincident points is refused by name. A constraint from an index to itself is unchanged.The debug adjacency check no longer skips a triangle list with a repeated directed edge; it panics on one (#300). Only duplicate points produced such lists, and they are merged now.
triangulatecovers the exact convex hull of its input (#299). The build bracketed the points with a finite super triangle (32 times their span) and dropped every triangle touching it, which lost the thin hull triangles whose circumcircles reached its corners -- 3,977 instead of 3,979 triangles for 2,000 random points, with nothing reported. The hull is now closed by one symbolic vertex at infinity, decided by the predicates without coordinates, so the output always has2n - 2 - htriangles fornpoints withhon the hull boundary, collinear ones included, and its boundary is the exact hull. Constrained triangulations recover their constraints in that complete triangulation.Where the Delaunay triangulation is unique (no four points cocircular) and the old build kept every hull triangle, the output is the same set of triangles as before; their order in the list changes, because the build starts from the first three points that span a triangle instead of from the super triangle. With cocircular points either build may choose a different, equally Delaunay triangulation.
0.3.3 - 2026-10-10
Changed
triangulatekeeps its adjacency current locally (#268): a split of a triangle into three, of an edge and its two triangles into four, and each Lawson flip -- after insertion, in constraint recovery and in the pass that restores Delaunay -- re-link only the twin pointers of the triangles they rewrite and of those triangles' outside neighbours, including the old neighbours of a reused triangle slot. The build used to recompute every twin pointer after each insertion and flip, so n points cost O(n^2) or worse. Debug builds compare the maintained adjacency with a full recomputation after every step (up to 1024 triangles outside the crate's own tests) and panic on the first difference.- Point location starts its walk next to the last vertex inserted in the point's cell of a coarse grid, instead of from the last triangle, whose walk crossed O(sqrt n) triangles per point. A point on an edge or a vertex is located by the old walk, so the output is unchanged: the same triangles in the same order.
- Together: 16,000 uniform random points took 72 s and take 18 ms; from 1,000 to 16,000 points the time grows 29x (n log n: 22x, the old build: over 300x). Measured by
tools/benchmark'striangulate_points.
0.3.2 - 2026-09-28
Changed
- The crates.io page is this crate's own
README.md, with links to its API documentation, its reference page and the source (ADR 0078).
0.3.1 - 2026-09-27
Fixed
triangulate: after recovering constraint edges, unconstrained edges are flipped back to locally Delaunay (Lawson's flips). Recovery used to leave long triangles whose circumcircles held points no constraint hid, so the result was not constrained Delaunay as documented -- with finely sampled walls, triangles spanned whole rooms (#139).triangulateno longer loops for ever on outlines like a square turned 45 degrees (#190): legalisation after inserting a point checked the new diagonal instead of the two edges across from the point, so the real edges were never checked and thin quadrilaterals flipped back and forth. It now checks those edges, flips only strictly convex quadrilaterals, splits the edge (and both triangles beside it) when a point lands on one, and carries a flip bound.- Constraint recovery no longer gives up at the first crossing edge it cannot flip (#190): it follows Anglada's queue, retrying edges that cannot flip yet and requeuing new diagonals that still cross, and reports
CrossingConstraintsonly when a full pass flips nothing.