Skip to content

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_lines draws 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 case perspective claim of 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) and application (dispatch and exact scalar providers) bundles (#371, ADR 0105). Application gains hidden_lines, audit_hidden_lines and visible_faces; ApplicationBuilder gains with_portable_hidden_lines and with_portable_visible_faces, and Application::portable() selects both. The descriptor advertises MESH_HIDDEN_LINES and MESH_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_features reports mesh-hidden-lines, dispatch-mesh-hidden-lines and hidden-lines-provider.

Changed ​

  • Breaking: ApplicationErrorSource and ProviderConformanceReport are #[non_exhaustive]. ApplicationErrorSource gains HiddenLines and ProviderConformance; ProviderConformanceReport gains MeshHiddenLines and MeshVisibleFaces. A match on either needs a wildcard arm (#371, ADR 0105).

  • Breaking: requires axiolid-overlay 0.4, so axiolid::overlay carries OverlayError as #[non_exhaustive] with the new HolesOverlap and PolygonsOverlap, and Region::new refuses rings that cross into each other. A match on OverlayError needs a wildcard arm (#448, ADR 0104).

0.4.7 - 2026-11-03 ​

Added ​

  • The package now ships LICENSE and LICENSE-COMMERCIAL (symlinks to the repository root), so the licence texts travel with the archive (#358).

  • mesh_hidden_lines re-exports ScalarVisibleFaces under the hidden-lines-provider feature (ADR 0094, #383).

0.4.6 - 2026-10-29 ​

Added ​

  • mesh_section exports CutRegion, CutRegionLimits and the cut-region refusal names, so Application::section_mesh_request can return the cut of a solid as closed regions (ADR 0092, #372).

0.4.5 - 2026-10-29 ​

Added ​

  • Application::section_mesh_request sections open meshes when the request has SectionRole::Sheet (ADR 0091, #359).

0.4.4 - 2026-10-28 ​

Added ​

  • Application::section_mesh_request sections 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-lines and dispatch-mesh-hidden-lines features, with the axiolid::mesh_hidden_lines module: the orthographic hidden-line drawing contract and its registry (ADR 0090, #138). Opt-in; the bundles do not include it.
  • hidden-lines-provider feature: 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 (was MPL-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 serde feature (#267, ADR 0085) forwarding to the serde feature of every representation package already in the build; with model it 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-mesh 0.4, so axiolid::ray_mesh (re-exported under the ray features) carries the new RayMeshError::TriangleIndexOutOfRange, and RayIndex queries refuse an out-of-range candidate instead of skipping it. An exhaustive match on RayMeshError needs 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 LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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-thread CpuExecution runs install on a local pool of one worker (#259). It ran the closure directly, on whatever pool the caller was in, so a provider reading rayon::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 from CpuExecutionBuilder starts the pool when it is built and reports a failure there; one from CpuExecution::portable or CpuExecution::detect starts it on first install. 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 LICENSE and LICENSE-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 (was MPL-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 is None unless 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 LICENSE and LICENSE-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 (was MPL-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::transformed refuses 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 new TransformError: a non-finite transform, a linear part not orthonormal within RIGID_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 LICENSE and LICENSE-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 (was MPL-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 than min(eps, rounding floor) for an exact report, the floor alone at Tolerance::ZERO, and eps otherwise. 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 at Tolerance::METRE, and at 1e-7 and Tolerance::ZERO the 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 TangentSplit or Assembly; 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_exactly proves 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 needs axiolid-brep with translated_exactly.

0.1.14 - 2026-11-07 ​

Fixed ​

  • BooleanReport::rounding_floor adds the move of an angular residue at the rounding floor (2^-40 rad, 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 between 2^-40 times the extent and the floor may be a reported decision, so a check of linear() <= rounding_floor() is about 4.5 times looser than before.
  • A floor-standing door 3e-7 to 9e-7 short 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 at 3e-7, 7e-7 and 9e-7 short and 3e-7 and 7e-7 past a straight wall, for a union and an intersection at 3e-7 either side, and for a curved wall at 9e-7. A door 1e-6 or 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 LICENSE and LICENSE-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 cuts 2e-8 apart, which were never merged for a traced section. They are now one cut within tolerance, reported as MergedPoints, so the box less the torus cuts at a micrometre; Tolerance::ZERO still 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::ZERO and, 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 (was MPL-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_FACTOR and BooleanReport::finer (#332): a boolean refused at the caller's tolerance is cut again at 2^-10 times it, and its report says so. BooleanReport::merged keeps 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 um of an arc of radius 24, and welded two vertices a split had kept apart ("two pieces leave a vertex of a split face in one direction"). A wall of radii 6 to 6.25 less five wedge openings, abutting by 15 um and 0.6 mm, and one of radii 24 to 24.1 whose openings abut by 7 um, are cut at the closed-form volume (within 1e-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 an UnsupportedContact before, 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.8 to 5 less a box on the tangent plane, or a unit in the last place either side of it, is cut at the closed-form volume (within 1e-12), its report within 1e-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 most 4e-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-form Sinusoid2 pcurve 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-6 to 5e-4 past 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, which extrude_profile_exact builds by reflecting the +z one (#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 angle 2 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 (MergedPoints above 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 at Tolerance::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::ZERO the report is empty again (#251, the #236 contract). A support reading the exact predicate rejects was taken when its f64 measure was exactly 0, and reported as a decision with linear 0: a round hole across a wall under a general rotation, whose axis and the wall's normals round to the same f64 vector though the numbers given (a frame orthonormal only to rounding) are not perpendicular, was read PlanePerpendicularToAxis at 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 tangency module):

    • 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 the PlaneTouchesCylinder reading. 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 reported PlaneTouchesCylinder distance. Exactly tangent pairs are still decided by the exact predicates, and succeed at Tolerance::ZERO with an empty report.

  • Cuts merged within tolerance on a circle or ellipse on both sides of its parameter origin (one point named at 0 and at 2 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^-20 of 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, at Tolerance::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::extent is the extent it was scaled from, with its definition (the largest max(|x|, |y|, |z|) over the operands' vertices and their analytic surfaces' origins widened by their radii), and the public ROUNDING_FACTOR (2^-40) is the relative factor. merged keeps the larger floor.

0.1.3 - 2026-10-03 ​

Added ​

  • boolean_with_report returns, with the result, a BooleanReport of the within-tolerance decisions that fired (#236): one ToleranceDecision per ToleranceDecisionKind (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. boolean is unchanged.

Fixed ​

  • Placed differences succeed at Tolerance::ZERO (#236). Naming a point on the curve or surface it was evaluated from compared its f64 round-trip residue with the linear tolerance, so at zero every boolean failed with BooleanError::Evaluation; that bookkeeping, and welding two evaluations of one vertex, now allow the rounding of the operands' extent (2^-40 of 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 0 and +-1 meet, 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-17 off 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 Tolerance only: distances its linear part, with no built-in 1e-9 floor, and the coincidence and contact tests on normals and axes its angular part instead of a fixed 1e-9. With Tolerance::METRE results 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, Sinusoid2 for oblique cuts).

axiolid-capi ​

0.4.0 - 2026-11-13 ​

Changed ​

  • Breaking for Rust callers only. The public C-mirroring enums AxiolidStatus, AxiolidOperation, AxiolidRepresentation, AxiolidExactness and AxiolidGeometryKind gain 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_patch 1).

Added ​

  • Visible faces through the C ABI (#386): axiolid_v0_4_visible_faces with AxiolidVisibleFaceLimits; the result is read with axiolid_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. AxiolidFaceSource distinguishes group regions from near-plane caps, AxiolidGeometryKind gains VisibleFaces, and the capability table lists MeshVisibleFaces. 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_lines with AxiolidView, AxiolidViewMesh (roles, group modes including planar), AxiolidEdgeSelection (including group boundaries) and AxiolidHiddenLineLimits; the result is read with axiolid_v0_4_hidden_lines_count, _copy and _evidence. axiolid_v0_4_context_last_error_mesh returns the id of the input mesh a refusal names. AxiolidOperation gains MeshHiddenLines and MeshVisibleFaces, AxiolidGeometryKind gains HiddenLines, and the capability table lists MeshHiddenLines. All arguments are validated at the boundary. abi_patch is 1; existing symbols and layouts are unchanged.

0.3.4 - 2026-11-07 ​

Added ​

  • The package now ships LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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-overlay 0.4, whose OverlayError is #[non_exhaustive] with the new HolesOverlap and PolygonsOverlap, and whose Region::new refuses rings that cross into each other. ProfileFinding::DegenerateRing carries that OverlayError, so a match on it needs a wildcard arm (#448, ADR 0104).
  • revolve_arc_ring refuses an axis origin with z != 0 as "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 (z zero 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 with ExactBRep::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 direction z or an origin z other than exactly zero) is UnsupportedInput ("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_ring refuses 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 sin and cos exactly at a quarter turn (#379): a parameter within 2 * 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 exactly 0 and +-1 in that coordinate, where it used to be up to 0.55 * t * EPSILON off (0.18 at 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 times max(1, |t|) in its scale, so an arc of a large parameter closed by a line between its raw ends still lowers at Tolerance::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_exact and revolve_arc_ring revolve 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 exact side_of_axis sign, 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 within 64 * EPSILON of 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 stays UnsupportedInput by 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 back Ok with its pcurves 7e-5 off their curves, and the same for a cone's apex edge. The exact revolution now refuses an edge whose rise * 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_ring mirrored 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_with and validate_contours_with take ArcCheckOptions; 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 a ProfileFinding::Clearance naming both segments (a narrow wedge at a nonzero angle is not reported), with a certified lower bound on their distance and an undecided flag for a pair the exact check could not clear. ValidateProfile::validate and the extruder are unchanged (#388, ADR 0095).

  • profile::paired_profile_rings and PairedRings (#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 returns None.

Changed ​

  • contour_to_arc_ring and the profile validator lower Curve2::Polyline contour 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; the TooFewSegments message says so (#389).

Fixed ​

  • extrude_profile_exact ran 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 as InvalidInput naming the defects; a nesting the exact check cannot decide is refused as UnsupportedInput. 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 was InvalidInput and 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.
  • The caps of extrude::extrude_profile and 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 and audit_mesh reads as an open mesh (#415). A sliver that is not flat (a fine chord's) is still meshed, as it is legitimate there. triangulate and triangulate_with are unchanged.

0.3.28 - 2026-11-03 ​

Added ​

  • profile_validate::ExactProfileValidator implements ValidateProfile for ContourProfile and Profile. 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 LICENSE and LICENSE-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 as revolve does; 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), is UnsupportedInput; an axis perpendicular to the plane is InvalidInput. 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_plan and pipe::fixed_reference_along_pieces sweep 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. Also reference_clearance, ring_reach and pipe::check_disk_along, which refuses a disk wider than a lone arc's bend.

0.3.24 - 2026-10-28 ​

Fixed ​

  • profile_rings ends 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 * t rounds), a curved endpoint; a tie keeps the earlier segment's end. An arc ending on the revolution axis used to land at x ~ 6e-17 (its end re-evaluated as centre + 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_deviation adds 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 ​

  • revolve meshes 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 with UnsupportedInput: 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 still UnsupportedInput, since its on-axis vertices are then off the real axis. A profile crossing the axis is still InvalidInput. tapered_revolve keeps its UnsupportedInput refusal for a profile touching the axis (#364).

0.3.22 - 2026-10-28 ​

Fixed ​

  • revolve and tapered_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 (was MPL-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_contour accepts 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 E air-terminal-element.ifc tube (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 allows 1e-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: a SlabDepth, Relative to the boundary's extent (as before) or the Reach of a subject. BoundaryPrism states where the boundary stands (origin, x, y, used as given) and the axis; BoundaryPrism::along_placement_z reads it from a placement, IFC's IfcPolygonalBoundedHalfSpace.Position, BoundaryPrism::along_normal from a plane frame as bounded_half_space_in_frame does, and BoundaryPrism::crossing the 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's x projected 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_frame honours a frame mirrored about the plane normal, whose y is -(normal x x) (#322): its boundary is built mirrored, as written, still outward-facing. It used to rebuild y as normal x x and 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_deviation is 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_face is triangulate_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). A SteinerPoint is 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), which SteinerPoint::place replays over any point type, so a caller lifting the face into space places it exactly in an axis plane. The result (ShellFaceTriangulation: points outer ++ 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 under Accept. 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 section back short 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. back and ahead are the least turns at which the section short of and past the corner clears the plane by 0.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, which pipe::pipe_plan serves (PipePlan::arc_mitres, ArcMitre, MitreSide).
  • Swept disks round closed directrices (#248).pipe::swept_disk_along_loop sweeps 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 the r 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 with BudgetExceeded), 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::Station and loft::Frame derive Debug and Clone.

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 Cylinder standing 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: an EllipticalCylinder with its axis along the unit direction d, semi-axes r (across the lean) and r d.z, and rims traced by Sinusoid2 pcurves (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 volume area * 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_profile returned a sliver solid for an offset within tolerance of the profile plane, and a flat zero-volume "solid" at exactly o.z = 0; it now refuses |o.z| <= tolerance with 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-compile meshes extrusions through extrude_profile, so it refuses them too. Test: both paths refuse z = 0, -0.0 and +-tol / 2, and build +-2 tol with the same closed-form volume. Mutation probe: scripts/probe_oblique_extrusion_mutants.py.

0.3.16 - 2026-10-09 ​

Added ​

  • profile::ring_touches and profile::RingTouch (#265): every vertex lying inside another ring edge, which triangulate_with under PinchPolicy::Accept inserts 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 ​

  • triangulate and PinchPolicy::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_exact refused any offset with offset.z <= tolerance as "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, through ExactBRep::transformed, for every profile family and for oblique directions. Negating z is 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.0 twin of its lowest corner (#269). The certified clipper reads a ring's turn at its lexicographically smallest vertex, found with f64::total_cmp, which orders -0.0 before 0.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.0 next to the origin's 0.0); a downstream triage found 35 refused rectangular faces in two models. Every coordinate comparison of the clipper now orders by value, -0.0 equal to 0.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::triangulate and offset::offset_solid key corners with -0.0 read as 0.0 too, 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::Refuse is triangulate (extrusion and loft caps, unchanged), PinchPolicy::Accept triangulates 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 in outer ++ 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 and n + 2h - 2c triangles for c parts. 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 by triangulate and extrude_profile by 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::triangulate delegated to earcut, 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 exact orient2d sign (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 with Degenerate otherwise. Rings may come either way round; every vertex is now a triangle corner, outer ++ holes unchanged. Ring sets that bound no polygon with holes are refused with InvalidInput naming 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 on earcut. 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_ring demanded bit-equal segment joints at Tolerance::ZERO, which a Line2/Circle2 contour cannot give for non-dyadic sizes: a line ends at a rounded origin + direction, an arc at the cos/sin of 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 at Tolerance::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^2 included; 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. In axiolid-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_pieces refused 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, as IfcSweptDiskSolid defines 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 by 1 / 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 tolerance joint_tolerance is 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::Chain profile 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, with pipe::PathPiece and pipe::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 than 2 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_deviation and ProfileDeviation (#232): how far a profile's exact boundary may lie from the rings profile_rings flattens 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_within and sweep::surface_curve_sweep_within, with sweep::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::revolve and sweep::tapered_revolve bound the distance from every point of the surface their rings sweep to the mesh by tolerance.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 with BudgetExceeded instead of met by fewer steps.

0.3.9 - 2026-10-02 ​

Added ​

  • Exact swept disks (#223). swept_disk_exact::swept_disk_along_line_exact sweeps a disk, optionally with a bore, along a straight segment into a capped cylinder; swept_disk_along_arc_exact sweeps 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 with ExactBRep::transformed. Volumes match pi r^2 L and 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; since angle and axis_direction only matter through their product (the axis-angle rotation vector), a sign-of-angle check 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 matches revolve_profile_exact's positive sign for either sign of angle and either axis direction. tests/revolve_partial.rs compared only |volume| between the two paths because of this; it now compares the signed volumes.
  • sweep::tapered_revolve shared the same unconditional station reversal as revolve::revolve and 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_exact now builds a revolution through 0 < |angle| < 2 pi instead 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 new revolve_partial::revolve_section_partial builds 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.py lists the faults the tests must catch.

Changed ​

  • revolve_profile_exact refuses 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_exact completes 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) * t rounded twice. The old formula cut the plane z = 1/3 at 0.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.
  • 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 with Tolerance::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_rings flattens every Profile::Section family -- I, asymmetric I, L, T, U, C, Z, trapezium -- from the exact contour section_contour builds, fillets and toe radii chorded within the budget, straight edges exact. It refused them with Unsupported { ProfileTriangulation } before. Rectangles with corner radii mesh with them, through rectangle_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, with SphereEvidence::error bounding 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-exact for 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_regions over one ArcArrangement (#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 one ExactBRep, 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 stay Cylinder faces trimmed by an exact Ellipse3 edge with a Sinusoid2 pcurve (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_solids and boolean_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_exact runs 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 above z = 0 is extruded from its own base height. Disconnected results go through the _solids variants.
  • Stepped coaxial booleans are built, not refused (#120, ADR 0072): boolean_prisms_exact, boolean_arc_prisms_exact and their _solids variants 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_exact builds 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_stepped docs: 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_exact and their _solids variants. A cavity in a result of several pieces is still refused by name.

Fixed ​

  • A sharp rectangle revolved through revolve_rectangle wrote 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_exact and 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 every Forward face pointed into the solid. The topological and geometric audits compare faces with each other and passed it; exact_properties measured -2 pi R A for every Pappus fixture. The frames are now (X, -Z, Y).

0.3.2 - 2026-09-25 ​

Fixed ​

  • sweep::swept_disk carries 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_sweep is unchanged: there the reference is the author's.

0.3.1 - 2026-09-24 ​

Fixed ​

  • extrude (and so extrude_profile and 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_frame now 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. ReferenceMeshCompiler passes BoundedHalfSpace.placement.translation as 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_contour builds 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 is Eq, for the snap-rounding evidence of visible face regions (#410).

0.3.6 - 2026-11-03 ​

Added ​

  • The package now ships LICENSE and LICENSE-COMMERCIAL (symlinks to the repository root), so the licence texts travel with the archive (#358).
  • The MESH_VISIBLE_FACES capability identifier (org.axiolid.geometry.mesh-visible-faces.v1) and the Operation::MeshVisibleFaces diagnostic variant, for the visible face region contract (ADR 0094, #383).

0.3.5 - 2026-10-28 ​

Added ​

  • The MESH_HIDDEN_LINES capability identifier (org.axiolid.geometry.mesh-hidden-lines.v1) and the Operation::MeshHiddenLines diagnostic 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 (was MPL-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 most base + 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) and ScratchRequirement::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 (Serial 1, Threads(n) n, Auto the available parallelism).

Changed ​

  • ScratchRequirement::fits_budget charges a per-worker term for Parallelism::worker_bound of the options. upper_bound_bytes reports None for 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_error and ExecutionOptions::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 at Tolerance::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 LICENSE and LICENSE-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 (was MPL-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 serde feature, off by default, deriving Serialize and Deserialize for Aabb, Frame2, Frame3, Interval, Plane3 and BooleanOperator, with glam's own serde support 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::height is exact to rounding for either sign of b (#414). It took the root as 2c / (-b - sign sqrt(D)), which cancels for one sign of b, losing about 1e-11 relative height near a vanishing a; it now takes c / q or q / a with q = -(b + sign(b) sqrt(D)) / 2, which adds terms of one sign. AngleGraph2::angle is unchanged: near a^2 + b^2 = c^2 its 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 LICENSE and LICENSE-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 (was MPL-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 fields PathPiece::start_bound and PathPiece::end_bound, set by PathPiece::between_parameters(curve, t0, t1), PathPiece::bounded(curve, start, end), PathPiece::with_start and PathPiece::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. The start or end beside a bound holds an infinity, which nothing reads; PathPiece::length is infinite and PathPiece::is_stated false 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 by PathPiece::to_end(curve, start) and PathPiece::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], and PathPiece::is_stated says whether every length a piece spans (its offset's base included) is stated. An open piece's end holds Scalar::INFINITY, unread, so PathPiece::length and CurvePath::length are infinite until an evaluator resolves it. Stated pieces are unchanged.

0.3.7 - 2026-10-10 ​

Added ​

  • An optional serde feature, off by default, deriving Serialize and Deserialize for Curve2, Curve3 and every value they hold, SeamSide included: 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 a PathPiece -- the span [start, end] of a PathCurve (an owned Curve2 or Curve3) 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. PathCurve and PathPiece are #[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, along normalise(V x T)) and OffsetLaw::Linear (offsets at stations, linear between the piece's ends, in an OffsetFrame), with PathOffsets and OffsetFrame. All #[non_exhaustive]. PathCurve::is_line is also true for an offset of a line by a constant law, and PathPiece::frame_is_exact also 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 = +1 about the right rail and -1 about the left (RailSide, RailSide::pivot_sign). A Viennese bend, a polynomial in the bank angle, now rotates about its low rail exactly; under BankConvention::VerticalRise the held rail head stands e0 above the profile at every station. Built with CantPiece::about_rail; CantLaw::has_rail_pieces finds one.
  • Banked3::cant_rate_at: dD/dd, the law's own rate or b cos(psi) psi' for an angle piece.
  • BankError::RailInCant: a held-rail piece in a cant law is refused by name.

Changed ​

  • Banked3::pivot_at reads a held-rail piece from the cant law: value e0 + s D / 2 and rate s D' / 2, with the cant law's refusals at that station. Height pieces read as before; an angle piece in the pivot law is still BankError::AngleInPivot. CantLaw::value_at and rate_at have 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), or Incoming, the piece that ends there. #[non_exhaustive]; shared by axiolid-model's stations and axiolid-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. CantLaw is a run of CantPieces from plan distance zero, each a CantForm over its own xi = s / length: a polynomial in xi (constant, linear, Bloss, and Helmert as two quadratic pieces through CantPiece::helmert), a half-cosine, a sine transition, or a Viennese bend, which gives the bank angle itself (CantValue::Angle). A second CantLaw of height pieces is the pivot: the rotation point's elevation above the profile. The rail-head distance b gives psi = asin(D / b) (bank_angle), and a cant beyond it is refused by name (BankError). BankConvention names how a cant is read on a grade, with no default: TangentRotation rolls the section about the 3D tangent by psi (the rail heads rise D cos theta), VerticalRise by asin(D / (b cos theta)) (they rise exactly D).
  • ElevationLaw::CircularArc { height, grade, radius } (#238): a vertical circular arc in plan distance, the circle itself rather than a parabola. With t0 = atan(grade) and signed R (positive sag), sin t = sin t0 + d/R, z = height + R (cos t0 - cos t) and grade = tan t, evaluated in a form that does not cancel for small d/R. height_at and grade_at answer it in closed form and report None where |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_at and grade_at report None for it, since its height is a quadrature, read by axiolid-evaluate.
  • ElevationLaw::circular_arc, ElevationLaw::intrinsic and ElevationLaw::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 any Curve2, written in the piece's local frame (through the origin with tangent +x at start), by arc length over length. 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, joins and piece_at read the data; evaluation lives in axiolid-evaluate. Elevated3 takes 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::value and SeriesField2::value evaluate the value alone, without the jet, and agree with jet to 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, reversed and solved_range, ImplicitCurve2::solve_cell, and Field2::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 has solve (the parameters on both surfaces and the point), rates, second_rates, sub, reversed, parameter_of and side, and pair_section::solve4 for the 4x4 systems behind it (ADR 0077).

  • BSplineSurface is defined here, and axiolid_surface re-exports it unchanged, so a traced curve can carry a B-spline carrier (Carrier::Spline). It has BSplineSurface::jet (point and first and second partials, rational) and domain.

  • Field2 is now an enum of SeriesField2 (the former struct: powers and harmonics) and PatchField2 (piecewise Bernstein polynomials on a grid, bounded by their coefficients over any box and continued past the grid by their edge polynomials). ImplicitCurve2::clipped cuts 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, closure and turning_points. implicit::{bound, bound_simple, partial} give interval bounds and partial derivatives of a Field2 over parameter boxes.

  • Curve2::Implicit(ImplicitCurve2) and Curve3::ImplicitSection(ImplicitSection3) (#119, ADR 0077): a stretch of a Field2'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 analytic Carrier (plane, ruled surface, sphere or torus).

  • Curve2::QuadraticGraph(QuadraticGraph2) and Curve3::RuledSection(RuledSection3) (#119, ADR 0076): one root branch of a(t) v^2 + b(t) v + c(t) = 0 with 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) and Curve3::TorusSection(TorusSection3) (#119, ADR 0076): the solution u(t) of a(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 graph v = 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: Curve2 is #[non_exhaustive].

axiolid-curve-evaluate-contract ​

0.3.9 - 2026-11-07 ​

Added ​

  • The package now ships LICENSE and LICENSE-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 (was MPL-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 as path_joint_distances certifies 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 to path_joint_distances and keeps the joints that meet the window, so a provider that does not certify joints refuses it naming PATH_JOINT_DISTANCES_UNSUPPORTED. window_span checks the window (a centre that is not finite, a window negative or not finite: InvalidInput) and DistanceInterval::meets tests 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, and PATH_PIECE_BOUND_UNSUPPORTED for 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_UNSUPPORTED names 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 at sqrt(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), one DistanceInterval per interior joint that contains the joint's true distance (the sum of the true lengths of the pieces before it) and is no wider than tolerance; lengths read from stored data sum to point intervals. A defaulted trait method, so no provider breaks: the default refuses by a typed UnsupportedInput naming the new PATH_JOINT_DISTANCES_UNSUPPORTED, never an estimate handed out as a certificate. DistanceInterval (lower, upper, #[non_exhaustive], built by new and point) 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_on and path_frame_at_on read a CurvePath (re-exported from axiolid-curve with PathPiece and PathCurve) as a composite station basis is read -- the distance end to end, every joint a seam read from the piece the SeamSide names -- and path_point_at, path_tangent_at and path_frame_at are their outgoing reading; path_distance_convention and path_frame_is_exact_at say 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 typed GeomError::UnsupportedInput naming the new CURVE_PATH_UNSUPPORTED, the plain ones delegate to them, the convention is Unsupported and 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, OffsetLaw and OffsetFrame from axiolid-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_on and frame_at_on take a SeamSide (re-exported from axiolid-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: Outgoing delegates to the side-less method, and every other side is refused by a typed GeomError::UnsupportedInput naming the new SEAM_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_at documents the axes the reference provider has always returned (#242): x the tangent, y up, z to the right. The text used to call z up. No behaviour changes; axiolid-evaluate pins the axes on a level line, a grade and a banked curve.

0.3.2 - 2026-10-03 ​

Changed ​

  • CurveEvaluator::frame_at documents 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 LICENSE and LICENSE-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 (was MPL-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.py showing 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 LICENSE and LICENSE-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 (was MPL-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 ​

  • MeshHiddenLinesRegistry and MeshVisibleFacesRegistry route by supports_projection: a provider that does not support the view's projection is skipped, and a request no provider supports is Unsupported before the topology audit. A perspective view needs a near below zero (InvalidInput otherwise). register_perspective_conformant registers a provider after the base suite and the perspective suite. register_conformant of both registries also runs the perspective suite when the provider claims perspective, so an unverified claim is refused (ADR 0107, #387).
  • MeshVisibleFacesRegistry accepts FaceGroups::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).
  • MeshHiddenLinesRegistry accepts EdgeKind::GroupBoundary segments only when EdgeSelection::group_boundary is set, and then refuses a PerTriangle group slice of the wrong length naming the mesh (ADR 0106, #385).

0.3.12 - 2026-11-03 ​

Added ​

  • MeshVisibleFacesRegistry (feature mesh-hidden-lines): registration, conformance-gated registration, fallback on Unsupported/Unavailable only, 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 LICENSE and LICENSE-COMMERCIAL (symlinks to the repository root), so the licence texts travel with the archive (#358).
  • MeshHiddenLinesRegistry::audit lists every input mesh that hidden_lines would refuse, in request order, without searching (#370). It shares the per-mesh audit with hidden_lines; refusals of the whole request stay Err.

0.3.10 - 2026-11-03 ​

Changed ​

  • MeshPlaneSectionRegistry::section_request no longer refuses cut regions returned beside a Both region; it checks that each Both region equals one cut region, and carries PlaneSectionEvidence::in_plane_crossings into the evidence it derives from the answer (ADR 0093, #366).

0.3.9 - 2026-10-29 ​

Added ​

  • MeshPlaneSectionRegistry::section_request checks cut regions: a provider must answer them exactly when asked, with canonical rings, budgets and evidence, and no Both region; a sheet request with cut regions is refused before dispatch (ADR 0092, #372). A provider that ignores the request and answers with cut_regions: None is treated as Unsupported, so the next provider is tried and a registry without one refuses.

0.3.8 - 2026-10-29 ​

Added ​

  • MeshPlaneSectionRegistry::section_request accepts open meshes for SectionRole::Sheet: boundary edges are allowed, degenerate, non-manifold and inconsistently wound input is refused before dispatch, the evidence must echo the role and the audited boundary_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 of section, 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_conformant also runs the region suite and rejects a claimed failure (#359).

0.3.6 - 2026-10-28 ​

Fixed ​

  • MeshPlaneSectionRegistry refuses a mesh whose index count is not a multiple of three with InvalidInput; it used to drop the partial triangle and could accept the rest as a closed solid.

Added ​

  • MeshHiddenLinesRegistry behind the mesh-hidden-lines feature: 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-lines is 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 (was MPL-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-section feature 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_execution is 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::boolean budgets 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_many and union_many already counted every operand.
  • A per-worker scratch term (ScratchRequirement::Affine) is charged for the width of the pool configured with with_execution when 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::ChainPositionTable and bound::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 an IntrinsicPositionTable; 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 to PIECE_FRAME_TOLERANCE off 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). None for a family without a certified arc length, a piece longer than its curve and a budget that runs out.
  • bound::ElevatedPositionTable takes an elevated or banked curve over a chain plan (#438; 0.3.21 returned None for one): the plan is read from a ChainPositionTable. point3_on(d, side) reads a join from either side, jet3_on returns 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) and jet_errors bounds how far the three can be from the exact ones.

Changed ​

  • chain::chain_certifies is false for 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, so flatten2 no longer claims such a chain.
  • bound::certifies_flattening3 claims an elevated curve over a chain plan again, exactly where chain_certifies does (#438; 0.3.21 refused it, #393, because its chord bound under-reported the evaluated ends).

Fixed ​

  • chain::chain_chord_bound, bound::chord_bound2 over a chain and bound::chord_bound3 of 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 certified ChainPositionTable point, 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 and k = 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 to PIECE_FRAME_TOLERANCE) is charged its jump.
  • curve::flatten2 and flatten3 no longer accept a span of a claimed family (certifies_flattening2/3) whose chord bound is None on its sagitta alone (#438): the span is refined and, at the depth limit, refused with BudgetExceeded. 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 resulting None bounds 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_bound2 bounds Curve2::Lifted pcurves (#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 from curve_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 of c'' and of the carrier's curvature along p', 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_bound2 bounds Curve2::Intrinsic between the evaluated ends (#414): the law's bound plus the larger distance of an evaluated end from a certified IntrinsicPositionTable point, with that point's own error. chain_chord_bound does 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), and SurfaceBoundOracle::separation with ParameterSeparation: a certified sigma with |S(x) - S(y)| >= sigma |D (x - y)| over a parameter box, by Weyl's perturbation from the box centre, or None at 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 an IntrinsicPositionTable, the height and the pivot in closed form, each point with a bound on its distance from the exact one. None for 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::FrameLaw bounds 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_bound2 bounds 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_bound2 of a point span (a == b) of a lifted or an intrinsic pcurve is the evaluated point's own certified error, no longer zero, and None where the curve has no bound (#414).
  • curve::flatten2 accepts 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_flattening2 still names neither family, whose bounds are not tight on a curved carrier.
  • bound::continuity_breaks2 and continuity_breaks3 name 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 by chord_bound2, as across a B-spline knot.

Fixed ​

  • bound::chord_bound3 of an elevated or banked curve over an intrinsic plan bounds the distance to the chord of the evaluated ends (#393), as chord_bound2 does 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 curvature 0.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, which certifies_flattening3 does not claim.
  • bound::certifies_flattening3 no longer claims an elevated curve over a chain plan (#393, TODO(#438)): the exact-chord bound of chord_bound3 under-reports the evaluated ends there (1.95e-5 against 2.8e-3 on a chain of a line and k = 0.01 sin 50 s), so flatten3 and 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_at and CompositeBasis::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 distance 0 and at the length.

  • FrameLaw::bend and BendBounds (#361): certified bounds, over a seam-free span, of the coefficients of the signed bend of a section frame law, O'.N, L.N' and U.N' (N the frame's tangent), in the section and the plan reading. A section point moves along the plane normal at O'.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::forward bounds O'.N from below for every frame with bounds. FrameLaw::has_bend tells a frame with a closed-form bend, which a narrower span bounds, from one without.

  • station::FrameLaw::error_at and StationFrameError (#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. None for a frame read by quadrature, a piece placed inexactly, a station within the resolution of a seam it is not on, and every family FrameLaw::bounds leaves unbounded.

  • FrameLaw::atomic_placed (#408): FrameLaw::atomic for 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_at stays None for such a piece.

  • bound::chord_bound2 bounds Curve2::QuadraticGraph and Curve2::AngleGraph pcurves (#414), which it left unbounded. The root w(t) of F(t, w) = 0 has w'' = -(F_tt + 2 F_tw w' + F_ww w'^2) / F_w; interval enclosures of F's partials (outward-rounded, from enclosed trigonometric coefficients) over the span by a certified height range bound it, on pieces of the span, and None is returned where the root may be undefined on the span or F_w is not bounded away from zero (a branch end nearby). Each computed end carries a certified root (a sign change of F in outward-rounded intervals), and an angle graph whose height box reaches the +-pi cut of AngleGraph2::angle is refused. Lifted pcurves (Curve2::Lifted) and Curve2::Intrinsic stay unbounded by chord_bound2; #414 remains open for them.

Fixed ​

  • FrameLaw::bounds, bend, has_bend and error_at read 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 the 64 eps (1 + K L) charged before, and a point could lie past its certified error.

Added ​

  • The package now ships LICENSE and LICENSE-COMMERCIAL (symlinks to the

  • bound::IntrinsicPositionTable and MAX_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 remainder w^5 / 4320 |f''''|, with `|f''''| <= sup|k'''| + 4 sup|k| sup|k''| + 3 sup k'^2 + 6 sup k^2 sup|k'|

    • sup k^4per 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.
  • FrameLaw bounds 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 an IntrinsicPositionTable, axes from the closed-form heading) into point_error and axis_error. piece_frame_bounds still returns None for 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 (was MPL-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_bound2 and offset_chord_bound3 evaluate and bound the offset B + d u of a curve at its basis's parameter (u the left normal in 2D, normalise(V x B') in 3D), sup |P''| from the basis's derivative suprema through the unit-vector rule station::certify uses for offset lengths (#294, #307), refusing a span across a break of the basis's C^2 continuity; intrinsic_chord_bound2, intrinsic_chord_bound3 and intrinsic_derivative_bounds2 bound 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. IntrinsicPositions reads 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 remainder w^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_PANELS caps the work. certifies_flattening2 and certifies_flattening3 are unchanged.
  • station::FrameLaw (#316, ADR 0082 amendment): certified frame-law bounds along an atomic curve or a CompositeBasis (CompositeBasis::frame_law), including pieces whose frames are read by quadrature, which piece_frame_bounds leaves 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_error and FrameLawBounds::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::certify refused naming UNCERTIFIED_LENGTH for every Curve3::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 no z component (compared with 0.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 of UNCERTIFIED_LENGTH now 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::certify refused naming UNCERTIFIED_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'' and P''' 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 / 12 over the speed (three times tighter than the turning bound, kept as a ceiling), and with |P'''| and the panel's own velocities to within h^4, so an ellipse, a B-spline and their offsets certify to 1e-8 relative and below within the unchanged budget of MAX_CERTIFIED_PANELS. A station 30 along the deck's offset certifies at a 1e-5 window's 1e-8 (refused before), and every interval the existing tests certify is narrower than before (by up to four times).
  • UNCERTIFIED_LENGTH no longer names an offset by distances along an ellipse or a B-spline.

0.3.14 - 2026-10-13 ​

Added ​

  • station::piece_frame_bounds, FrameLawBounds and FrameReading (#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. None for 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), and ReferenceCurveEvaluator::path_joint_distances_within through it, returns the joints whose certified interval meets [around - window, around + window], each bitwise as joint_intervals gives 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 naming UNCERTIFIED_LENGTH. The seam-snapping window's test of an interval against a window is DistanceInterval::meets, shared with it.

  • Curve-path piece bounds at a parameter or a point (#304, ADR 0082 amendment): StationPiece::from_path_piece, and so CompositeBasis::from_path and every path_* query of ReferenceCurveEvaluator, resolves a PathPiece::start_bound or end_bound. A point is resolved to the one parameter naming it (StationCurve::parameter_at_bound, module station::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 measure StationPiece::between_parameters reads; 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 path station::curve_path flattens such a trim into, and its joints are certified by joint_intervals as 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 so CompositeBasis::from_path and every path_* query of ReferenceCurveEvaluator, resolves a piece whose open_end is set to the curve's end by this evaluator's own length, the end StationPiece::whole reads: 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::path hands it back), which is the path a graph compiler flattens; path_joint_distances certifies 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, and CompositeBasis::joint_intervals, one DistanceInterval per 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 with M a certified bound on |P''| (from bound::curve_derivative_bounds* and the frame's unit-vector derivatives) every tangent on a panel lies within h 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, within h^3 / 24 sup |f''| from suprema of the curvature law's and the profile's derivatives. Uncertifiable lengths are refused by a typed UnsupportedInput naming UNCERTIFIED_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) as BudgetExceeded.
  • ReferenceCurveEvaluator::path_joint_distances answers with CompositeBasis::joint_intervals.
  • Seam-snapping windows (#294): station::window, with snapped_distance2, snapped_distance3 and CompositeBasis::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 (to WINDOW_CERTIFICATION of the window); a window holding two seams is refused naming SEAM_WINDOW_AMBIGUOUS, one with a seam straddling its edge naming SEAM_WINDOW_STRADDLED, a negative or non-finite window as invalid input. A window of 0 reads the distance as it is.

0.3.10 - 2026-10-10 ​

Added ​

  • surface::invert (and so locate) and surface::jet take an elliptical cylinder (#287): the angle is that of the section coordinates over the semi-axes, the inverse of evaluate's affine image of the circle. They were refused as unsupported. project still refuses it: the closest point on an ellipse has no closed form.

  • ReferenceCurveEvaluator reads curve paths (#290, ADR 0082 amendment): the contract's path_* queries build CompositeBasis::from_path and answer with CompositeBasis::section_on for 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 as CompositeBasis::new does) and CompositeBasis::path; StationPiece::from_path_piece, StationCurve::from_path_curve, and From<StationPiece> / From<StationCurve> for PathPiece / PathCurve (the curve copied).

  • Offsets as station bases (#289, ADR 0082 amendment): station::offset, with StationOffset (an offset of one base piece by an OffsetLaw, measured in its own length: arc length, or its own plan length beside an elevated or banked curve), StationCurve::Offset (appended last), StationPiece::offset and offset_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 radius r - a (length in closed form); every other offset is read numerically to OFFSET_TOLERANCE and 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 so from_path and the reference provider's path_* queries) reads offset pieces, building each one's measure table once.

0.3.9 - 2026-10-09 ​

Added ​

  • ReferenceCurveEvaluator reads a seam side (#286, ADR 0082 amendment): point_at_on, tangent_at_on and frame_at_on answer a measure ON a seam by the station rule (within ARC_LENGTH_TOLERANCE * max(1, s), for both sides) with station_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 +Z only, 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 typed UnsupportedInput.
  • Banked curves rotating about a held rail (#279, ADR 0081 amendment): a pivot piece CantForm::AboutRail is evaluated from the cant law, point, tangent and section alike. banked_second_derivative adds e'' = s D'' / 2, for an angle piece s (b / 2) (cos(psi) psi'' - sin(psi) psi'^2). banked_derivative_bounds and banked_chord_bound take 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) with P_k the bend's exact derivative suprema over the span (its ends and critical points), so flatten3 and the sweep certification carry over. banked_breaks and grade_corners3 name 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: CompositeBasis measures stations along StationPieces -- 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 within JOINT_TOLERANCE (relative), a gap and an undeclared reversed piece refused by name; every joint is a seam read by the #263 rule (section_on with a SeamSide); seams / exact_seams list the joints and each piece's own seams; frame_is_exact_at claims an exact frame only where every piece up to the one read is an exactly placed line; pieces_between clips 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 new KEEPS_UP_TOLERANCE), the placed curve's own reference-up frame of the moved point and tangent where it tilts +Z on an arc-length-measured source; an elevated or banked source so placed is refused by a typed UnsupportedInput.
  • 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::placement is the rigid motion taking local x, y, z onto the tangent, the left lateral and up and the origin onto the point (a linear placement's reading, not the provider layout of SectionFrame::frame); SectionFrame::moved carries a frame by a rigid motion; station_frame_is_exact2 / station_frame_is_exact3 say 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_seams3 list 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 -- as StationSeam { distance, parameter, smooth, exact }, without evaluating the curve; exact_station_seams2 / exact_station_seams3 refuse an inexact seam with a typed UnsupportedInput.

  • 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 the SeamSide names, 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::between is None within SEAM_TANGENT_TOLERANCE and refuses a near reversal (MITRE_TOLERANCE on the cosine of half the turn) and a seam whose sides do not share their point by name; Mitre::place projects a section point placed in each side's frame along that side's tangent onto the plane.

Changed ​

  • station_section2 / station_section3 read a station on a seam (within ARC_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_derivative3 and domain3 read a Curve3::Elevated by PLAN distance (ADR 0082, like Banked3): 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_derivative3 also reads a Curve3::Banked (banked::banked_second_derivative, the pivot's e'' added).
  • elevated module (#252): elevated_derivative, elevated_second_derivative, elevated_span, and the certified elevated_chord_bound -- sqrt(P^2 + Z^2), the plan's Taylor chord bound h^2/8 sup |k| and the profile's elevation_chord_bound at equal plan distance, orthogonal parts composed in quadrature (the module documents the derivation and why the plan's own chord_bound2 does not compose) -- with banked_chord_bound, elevated_derivative_bounds and banked_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, and grade_corners3, the seams across which the grade jumps, read from the laws on either side. bound::chord_bound3, curve_derivative_bounds3, continuity_breaks3 and certifies_flattening3 dispatch to them, so flatten3 certifies an elevated curve over a certified plan with a closed-form profile.

Changed ​

  • elevation: an intrinsic reading carries its profile arc length, and frenet'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_bound2 bounds implicit curves (Curve2::Implicit, ADR 0077) one cell at a time (#249), and continuity_breaks2 names 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_section2 and station_section3 resolve a distance along a curve -- plan distance on an elevated or banked curve, arc length (through arc_parameter) on any other -- to a SectionFrame: the point, the unit tangent, the lateral axis to the left and up = tangent x lateral. A 2D curve is framed in z = 0 with +Z up, a banked curve by its rolled section, every other 3D curve by the reference-up frame against +Z that ReferenceCurveEvaluator returns. SectionFrame::place applies offsets, plan gives the upright frame, frame the provider's layout (x tangent, y up, z right). station_length2/station_length3 give 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_tangent and banked_section evaluate a Curve3::Banked by plan distance. A BankedSection carries 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, with frame() and rail_heads(). Every refusal of the cant laws is passed on by name.

  • evaluate3, derivative3 and domain3 take a banked curve, parameterised by plan distance over its cant law's span, so flatten3 flattens it; certifies_flattening3 reports it uncertified (midpoint sagitta, #232).

  • ReferenceCurveEvaluator measures a banked curve in plan distance, and its frame_at returns the curve's section frame. An evaluator built against a reference up other than +Z refuses a banked curve, whose cant is measured against +Z.

  • elevation (#238): elevation_height and elevation_grade read every elevation law, alone or inside a piecewise one. Closed forms go through ElevationLaw::height_at/grade_at; an intrinsic (clothoid) profile is integrated with intrinsic_point and its plan distance inverted by a bracketed Newton solve to INVERSION_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''| with z'' = k / cos^3 t, for polynomials, circular arcs and intrinsic profiles; None across 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) and parameter_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 to ARC_LENGTH_TOLERANCE (1e-12) relative to max(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 past MAX_PANELS are refused by name. Against the binomial series of a cubic parabola the inverse lands within 1e-13 relative.

  • chain (#239): chain_point and chain_tangent evaluate a Curve2::Chain by arc length; evaluate2, derivative2 and domain2 take a chain, and an Elevated3 over a chain evaluates by plan distance (elevated_point, elevated_tangent, ReferenceCurveEvaluator, and so Curve3::Banked over it). A piece longer than its curve, a parametric piece off its local frame by more than PIECE_FRAME_TOLERANCE, a malformed chain and a distance outside the chain are refused by name.

  • Chains in the certified flattening (#232): chord_bound2 bounds a span inside one piece (an intrinsic piece by h^2/8 times a bound on its curvature law, a parametric piece by its curve's own chord bound between the parameters the span reads), continuity_breaks2 names the joins, and certifies_flattening2 accepts a chain whose intrinsic pieces have bounded laws and whose parametric pieces are certified families with no corner.

Changed ​

  • elevated_point, elevated_tangent and banked_derivative read the elevation through elevation_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_bound3 bound 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 form h^2/8 (|A''| + R|w''|) / w_min, which carries no first-derivative terms); curve_derivative_bounds2/3 bound |c'|, |c''|, |c'''| of lines, circles, ellipses, sinusoids and B-splines (rational too) from closed forms and derivative control polygons; SurfaceBoundOracle bounds 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/3 name the knots where a curve may fail to be C^k; certifies_flattening2/3 name the families whose flattening is certified.

Changed ​

  • flatten2 and flatten3 accept a span only when its certified chord bound is within the tolerance too, for every family bound covers (#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. A Curve2::Lifted reading a pair section on one of its own B-spline surfaces takes that surface's parameters straight from the solve.

  • surface::locate, curve::locate2 and curve::locate3: parameters of a point, iterating where no closed form exists (B-spline surfaces and curves: seeded Newton, verified by the round trip). invert, invert2 and invert3 keep their closed-form-only contract.

  • Evaluation, derivatives and inversion of Curve2::Lifted. An ImplicitSection on a B-spline carrier is inverted through the surface's locate.

  • Evaluation, derivatives and inversion of Curve2::Implicit and Curve3::ImplicitSection (ADR 0077). invert2 and invert3 now also cover QuadraticGraph, AngleGraph, RuledSection and TorusSection, reading the angle off the point and trying whole turns.

  • Evaluation, first and second derivatives of Curve2::QuadraticGraph, Curve3::RuledSection, Curve2::AngleGraph and Curve3::TorusSection (#119, ADR 0076); a parameter outside the graph's spans is refused, not extrapolated.

  • Curve2::Sinusoid evaluation: 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 LICENSE and LICENSE-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 (was MPL-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, and FixedInterval::sin_cos, certified sin and cos of 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-rounded f64, the fast tier), Dyadic (big-integer mantissa times a power of two, the exact tier), and certify, which runs one SignExpr in 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} and compare_along.
  • Tower and Nested: values with any number of nested square roots (capped at depth 6), with exact signs by recursive case analysis and an interval filter through Arith::sqrt_enclosure.
  • IntPoly and RealRoot: 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 sound f64 interval; 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, Faces and FaceId (#490, ADR 0109): the face on each side of every piece of an arrangement, nested components included, numbered from the unbounded face 0; with V vertices, E pieces and C components the count satisfies V - E + F = 1 + C.
  • Nearest and, on ExactPoints, nearest_on_segment, foot, compare_distance and compare_line_distance: exact distance predicates with no square root, decided through the interval tier and Dyadic.

0.1.1 - 2026-11-07 ​

Added ​

  • snap_round, Snapped, ClassBoundary, HOT_POINT_SCRATCH_BYTES and FRAGMENT_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. Snapped carries the displacement bound in the maximum norm and the number of rerouted edges, and Snapped::class_boundary nets 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, and ExactPoints::frame_sign.

0.1.0 - 2026-11-03 ​

Added ​

  • The exact point store, segment arrangement and ring tracing that the plane section of axiolid-reference used (ADR 0093, #366), as a crate of their own, so that the visible faces of axiolid-hidden-lines decide on the same exact signs (ADR 0094, #383).

axiolid-exact-compile-contract ​

0.3.3 - 2026-11-07 ​

Added ​

  • The package now ships LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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::SplitTJunctions and split_t_junctions split 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_intersections decides 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 the orient2d it relies on overflowed. The fix is in axiolid-predicates (#404).

0.3.3 - 2026-11-03 ​

Fixed ​

  • self_intersections no 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 float n_p x n_q direction, which is noise for nearly parallel planes. Every decision is now a certified orient3d or orient2d sign: 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. diagnose still flags such a triangle as DegenerateElement (#399).

Added ​

  • The package now ships LICENSE and LICENSE-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 (was MPL-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_fates names every input channel's fate.

axiolid-hidden-lines ​

0.1.5 - 2026-11-14 ​

Added ​

  • ScalarHiddenLines and ScanHiddenLines draw perspective views and claim them through supports_projection, replacing the by-name refusal of 0.1.4: a pinhole at the frame origin, image (U / -W, V / -W), near required 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 whose near is missing, zero, positive or not finite with InvalidInput. 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-lines the 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_j along 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 ​

  • ScalarVisibleFaces resolves FaceGroups::Planar exactly before labelling (new groups module: union-find over edges of exactly two triangles with parallel, same-facing normals, polled for cancellation, memory-budgeted; ADR 0106, #384).

  • ScalarHiddenLines draws EdgeKind::GroupBoundary when EdgeSelection::group_boundary is 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, input perspective view) when called directly, and do not claim it through supports_projection (ADR 0107, #387).

Changed ​

  • The providers' view-space vertices carry exact homogeneous coordinates [U, V, W, h] with h = 1 in an orthographic view, preparing perspective (ADR 0107, #387). The drawings are bit for bit the same: the segment hash of every scenario/hidden-lines case 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 LICENSE and LICENSE-COMMERCIAL (symlinks to the repository root), so the licence texts travel with the archive (#358).
  • ScalarVisibleFaces, the exact provider for MeshVisibleFaces (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 of axiolid-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. On scenario/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 ​

  • ScalarVisibleFaces no longer refuses a scene as Degenerate when 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 reports snap_displacement, rerouted_edges, collapsed_regions and collapsed_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 prepare step 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 (orient2d with 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 their candidate_pairs and exact_escalations evidence 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 every scenario/hidden-lines case. 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, a max_candidate_pairs limit 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-spatial BVH over view-space boxes instead of a linear scan. The predicate, the candidate set, its ascending order and candidate_pairs are unchanged (#138). Wall clock from scenario/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 Solid that the near plane cuts no longer fails as UnsupportedInput. A point with W < 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 for MeshHiddenLines (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 a Solid mesh is refused as UnsupportedInput naming it.

axiolid-inspect ​

0.3.10 - 2026-11-13 ​

Added ​

  • thin_sheet and ThinSheet: 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_coplanar and NearCoplanar: 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_winding and WindingError: bracket the winding excess E = integral of w(w-1)/2 = (integral of w^2 - integral of w) / 2 of a closed oriented cycle, an upper bound on the volume where its winding number is neither 0 nor 1 (equal to it where w is in {-1..2}), and accept it within a positive budget. Edge-touching shells and cavities pass; duplicated, overlapping and inverted shells are refused, so cycle_volume_properties can be trusted as the enclosed volume (#420).

0.3.8 - 2026-11-03 ​

Added ​

  • The package now ships LICENSE and LICENSE-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_gap delegates to it after its containment check (#356, #357).

Fixed ​

  • min_gap and 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 certified orient3d test and reads as Some(0.0) (#390).

0.3.7 - 2026-10-28 ​

Changed ​

  • The licence is now MPL-2.0 OR LicenseRef-Commercial (was MPL-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_volume and difference_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 near 1e30, 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 bracket 5.9e-14 wide, and the difference from [0, 0.8008] to [0, 5.9e-14].

0.3.5 - 2026-09-30 ​

Added ​

  • topology (#144): ComponentTopology::homology_basis now also covers a closed non-orientable component and any component with boundary, orientable or not, not only the closed orientable case -- k generators for k crosscaps, and 2g + (b - 1) or k + (b - 1) with b >= 1 boundary loops, all with Z2 (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 } or NonOrientable { crosscaps }). A closed orientable component also gets a basis of its first homology: 2g simple 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). genus is 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. Each DetectedPlane gives its triangles, a point and unit normal, a deviation that 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 (by orient3d), and its area. Largest first; deterministic.

0.3.1 - 2026-09-27 ​

Added ​

  • intersection_volume, difference_volume and enclosed_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 a VolumeInterval certified 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 LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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 serde feature, off by default, deriving Serialize and Deserialize for Line and Polyline: 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 LICENSE and LICENSE-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 (was MPL-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::Below could be claimed within about 1e-8 of 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-9 past 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) (L the sum of its degrees, about 3.5e13 for a bilinear patch) is refused with ExactMeasureError::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-12 of them. At georeferenced coordinates (450000, 5600000) the old margins left an interval about 1e-5 wide that never closed to the accuracy asked; a search now settles at a floor of 128 ulps of its scale, about 3.2e-7 there and 6e-14 at 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-12 of them: items of one body apart by more than that are measured instead of refused as ItemsNearlyShareFace, and at unit scale a 1e-13 gap 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_properties measures 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 to volume_properties on two-manifold input and refuses anything open with CycleVolumeError (#416).

  • select_by_normal and extent_along select the faces of a mesh whose normal lies in a NormalCone (the tolerance's angular part is slack on the half-angle) and measure their area and their extent along a direction. Selecting every face reproduces surface_properties area bit for bit (#417).

  • The package now ships LICENSE and LICENSE-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 (was MPL-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_properties measures 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 a 5.6e6 m reach and the noise floor of every curved-face integral was set from that reach: a 0.57 m^3 box at (6.0e5, 5.6e6, 204.5) measured 0.544 to 0.664 m^3 depending 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-10 m at 5.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 so MeshMeasure's second_moment_diagonal) sums about the mesh's first vertex and shifts the moments back to the origin by the parallel-axis terms, as volume_properties already 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) measured 360016800160 against 360016800196.08 and lost its central moments entirely. It now matches the closed form to rounding.

0.3.10 - 2026-10-09 ​

Changed ​

  • boundary_distance, boundary_clearance and the body_* 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 the boundary_distance benchmark 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_clearance and body_plan_overlap take a PlacedBody (or a slice of items) per side, as the body_* 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_overlap answers BodyPlanOverlap: 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's z: a body turned about z measures as its plan turned, and a tilted body casts its tilted items' shadows.

Fixed ​

  • plan_overlap no 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 ExactBRep per item in a slice: body_boundary_distance and body_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_hausdorff and its _with_budget form (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's ArcArrangement, now an optional dependency under exact), 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 new BodyMeasureError: ContactPlaneNotAxisNormal (items touching on a plane no coordinate axis is normal to, such as walls turned in plan), ItemsNearlyShareFace with 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) and ItemsOverlap. Every body_* query takes each side as a PlacedBody: 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 as ContactPlaneNotAxisNormal, and a placement that is not rigid as Placement. 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_distance took its matched fast path only for faces with identical pcurves (#227). Prisms from boolean_arc_prisms_exact are 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 exact feature. boundary_hausdorff_distance returns BoundaryHausdorff: intervals certain to contain the two-sided Hausdorff distance between the boundaries of two ExactBReps and both one-sided ones, refined to a requested absolute accuracy; one_sided_boundary_hausdorff measures one direction. Each interval is a HausdorffBounds, 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_distance returns MeshHausdorff: 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_hausdorff measures one direction. Each HausdorffBounds carries 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 with HausdorffError.
  • Hausdorff distance between polylines (#147), one-sided and two-sided, in 2D and 3D: polyline_hausdorff_distance, one_sided_polyline_hausdorff_distance and their _2d forms. 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_certified and frechet_decide_certified_2d answer whether the Fréchet distance is at most eps only when rounding cannot change the answer: FrechetDecision::AtMost, MoreThan, or Undecided within the error margin of the floating-point decision.

0.3.4 - 2026-09-30 ​

Added ​

  • Plan relations between exact bodies (#217). plan_boundary_distance and plan_boundary_clearance certify the distance between the bodies' projections onto the XY plane, with the contract of boundary_distance: the interval contains the distance, and the witnesses lie on the boundaries with their projections upper apart. 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_overlap returns PlanOverlap::Overlapping with a plan point inside both shadows when two planar faces, not vertical, share an open patch in plan (a column on a slab), Disjoint with a certified gap, or Undecided.

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 decision frechet_at_most, each with a _2d form. Empty polylines, non-finite points and an invalid leash are FrechetErrors. 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::Implicit pcurves, isolated with interval bounds, so faces trimmed by them get a certified domain.

Fixed ​

  • FaceDomain::contains counts 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::contains decides 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 answering None. 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_distance and boundary_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 answers Clearance::Below, Above or Indeterminate. 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_properties measures 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 ​

  • ExactMeasureError is 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 -- is NonPlanarFace, whose name predates curved faces, with the reason. The display text reads "cannot integrate a face (reason)".
  • The exact feature now also enables axiolid-evaluate and axiolid-curve.

Fixed ​

  • FaceDomain and 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.2 was tried at -0.2 - 2 pi, not at 2 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_properties honours face, shell-use and bound orientation. It read loop winding alone, which is only right for faces used forward; a Reversed cap off the plane z = 0 added its volume instead of subtracting it (a unit cube at 2 <= z <= 3 measured 7/3). Every solid tested before sat on z = 0, where the error vanishes.

axiolid-mesh ​

0.3.6 - 2026-11-04 ​

Added ​

  • audit_mesh_cycle and try_audit_mesh_cycle return MeshCycleHealth: the MeshHealth plus 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 LICENSE and LICENSE-COMMERCIAL (symlinks to the repository root), so the licence texts travel with the archive (#358).

  • split_triangle_at_side_corners re-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-compile and axiolid-heal shared 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 (was MPL-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 serde feature, off by default, deriving Serialize and Deserialize for TriMesh, PolygonMesh and 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's Surface_mesh (edge e owns halfedges 2e and 2e + 1, so opposite stores nothing). Built from a TriMesh (from_tri_mesh) or polygonal faces (from_faces) keeping the input numbering, converted back with to_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 circulators outgoing_halfedges, incoming_halfedges, vertex_vertices, vertex_faces; face and hole loops face_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; plus fill_hole and compact, which renumbers densely and returns a HalfedgeRemap.
  • HalfedgeMesh::validate checks every structural invariant and reports a HalfedgeInvariantError.
  • HalfedgeBuildError names the input a halfedge mesh refuses: NonManifoldEdge, NonManifoldVertex (a bowtie or two closed fans at a point), InconsistentOrientation, DegenerateFace, FaceTooSmall, IndexOutOfRange, IncompleteTriangle. HalfedgeEditError names 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, mirroring NormalAttribute::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, None for an unmapped corner), and AttributeChannel::UNMAPPED for triangles that carry no value.
  • validate_structure checks corner channels: whole tuples, one entry per corner, entries in range, and each triangle fully mapped or fully unmapped. New MeshValidationError variants 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): AttributeChannel gains the public field corner_indices, so struct-literal construction must add corner_indices: None; AttributeChannel::new is unaffected. DropReason is now #[non_exhaustive], so an exhaustive match on 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 BudgetExceeded before 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 SymmetricDifference charges each step the union and intersection it holds, instead of passing every step the whole budget.
  • union_many and subtract_many give each boolean the budget the batch does not hold, split evenly between the pairs of a parallel-batch level, 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_probe measures crossing combs, slab soups and pierced slabs (scratch_probe combs with no budget, scratch_probe crossing against budgets), and tests/scratch_bound.rs checks them.

0.3.5 - 2026-10-13 ​

Fixed ​

  • A memory budget holds for union_many and subtract_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 with parallel-batch a 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 a parallel-batch level 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 with BudgetExceeded before it starts. Before, union_many was charged once for its input triangles and subtract_many not at all beyond each boolean's own check, which only ran with parallel.
  • A boolean given a budget directly, without dispatch, is checked in every build, not only with parallel.

Changed ​

  • union_many borrows the solids that need no channel conformance and moves an odd trailing solid to the next level instead of copying it, and subtract_many borrows 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. With parallel-batch alone the term is the 16 KiB of a worker's bookkeeping.
  • The scratch_bound test and the scratch_probe binary 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.rs pins what each step is charged, and scripts/probe_scratch_bound_mutants.py kills 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 declares ScratchRequirement::Affine with a 64 KiB base, 1,536 bytes per input triangle of all operands, and 16 KiB per worker with parallel (plus the base per worker with parallel-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 of scratch_requirement has the table.
  • With a memory budget, a boolean (and a parallel-batch union_many) re-checks that bound against the width of the rayon pool it runs in and refuses with BudgetExceeded when the pool is wider than the budget check before dispatch assumed.

Changed ​

  • tests/scratch_bound.rs and the scratch_probe binary 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.py kills 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, through union_many and on the fast winding path, with the exact volume and a closed, consistently wound result (tests/overlapping_grid.rs).

Changed ​

  • tests/solve_failure.rs is replaced: no admissible input is known to reach a refusal inside the solve now, so the mapping to BackendContractViolation is pinned by a unit test where compute_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 as GeomError::BackendContractViolation naming boolmesh, not Degenerate. 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.rs pins 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_bound test 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 are UNMAPPED. Fates: Preserved when nothing was derived, Interpolated otherwise, Dropped(NotBlendable) for a Blend::None channel a cut needs. Every path carries channels (#116, completed): the analytic box path (sources recovered by plane lookup), grouped subtract_many (fused cutters keep theirs), tree union_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_edge pushed 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). The ProviderLimitation comment 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 LICENSE and LICENSE-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 (was MPL-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_requirement says 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 with BudgetExceeded before 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_requirement says 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 (the subtract_many/union_many defaults) and symmetric_difference_via_composition now 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 reach BudgetExceeded below 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_POINTS for 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 ElevatedPositionTable over 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 the Degenerate message 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, LinearExtrusion or Ruled mesh when every edge use carries a pcurve (#411, ADR 0103), under the families surface of revolution, surface of linear extrusion and ruled 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 at 0 (#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_ROUNDING with 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 was a 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 relation before any meshing, and a swept curve that is not a line, circle, ellipse or B-spline is refused as a swept curve other than a line, circle, ellipse or B-spline (a swept curve that is not an atomic 3D curve for 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-boolean with BooleanError::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 was Unsupported with no input. The exact compiler names the new cases: exact intersection of half-spaces without a subject: ... and exact 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 ElevatedPositionTable built 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 curvature 0.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 about 1e-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::Boolean contribution under MESH_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 fail meets_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 Solid with 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 is TODO(#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 as tool: mesh has no triangles or subject: mesh has no triangles (mesh), or as Degenerate (exact). The exact compiler still refuses an empty root, Degenerate, since an ExactBRep cannot 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 as Degenerate, 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 reports EMPTY_RESULT at 0, 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 ​

  • PolylineCompiler compiles 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. Requires axiolid-evaluate 0.3.20.

  • station::placement_accuracy, PlacementAccuracy and PlacementBound (#405): how far a node placed at a station lies from its exact placement, Exact on a line, Certified with the error of the placement's point and of each axis (PlacementBound::displacement for a local point), or Unbounded by name. Requires axiolid-evaluate 0.3.20.

  • SurfaceRelation::Ruled (#402) compiles to an open sheet (MeshClosure::Surface) with a certified deviation, the new DeviationPath::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 in ruled.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) or UNMERGED_UNION_CONTACT refusal. The result is accepted only as a closed solid that passes both guards, and is reported under the new details deviation::REFEREED_UNION and deviation::REFEREED_UNION_EXACT, with COINCIDENT_OPERANDS where 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 Certified deviation instead of Unbounded (#361, ADR 0082): the profiles are flattened together by paired_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. Requires axiolid-construct with paired_profile_rings.

  • deviation::TOLERANCE_READINGS and deviation::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_INTERSECTION and REFEREED_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_path and 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, by 1e-12 * max(1, |x|), the evaluator's rule for a distance); by more it is InvalidInput.

  • 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 F grows every contribution of the instance, as the placement's rounding does, and is named once as a certified StationPlacement contribution. The source is re-meshed with room for the rounding and F when 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 UnsupportedInput for Tessellation (a B-rep face on a surface relation), no longer as InvalidInput; 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 UnsupportedInput for SurfaceEvaluation (a curve-bounded surface over a surface relation, a surface relation as a reference surface), no longer as InvalidInput (#402).

  • A union of differently composed turns that was refused by name (UNMERGED_UNION_CONTACT or 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 BudgetExceeded at 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's parameter_range on 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/r of a circle, 1/|d| of a line, 1/max semi-axis of an ellipse, the evaluator's derivative bound of a B-spline, 1 of 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 to 1e-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-evaluate 0.3.20 for FrameLaw::atomic_placed and FrameLaw::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::Degenerate starting with UNMERGED_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-evaluate 0.3.19). A 5 km alignment at a 1 mm chord error compiles and certifies in about 370 ms. The UNCERTIFIED_FRAMES text 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 LICENSE and LICENSE-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-boolean 0.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_deviation within 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 by compile_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-3 measured 0.95e-3 and was reported 1.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 chord 1e-3 reports a Certified bound within the budget. A tool that is itself a boolean is still refused by name (#326). An intersection that leaves nothing is Degenerate naming 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 Revolution of 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 Revolution whose profile touches the axis now compiles to a closed mesh instead of failing with UnsupportedInput (#363, from axiolid-construct), unless a full turn touches the axis at an isolated vertex, which pinches the solid and stays UnsupportedInput; a TaperedRevolution touching 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 Revolution or TaperedRevolution whose profile crosses the axis now fails with InvalidInput instead of compiling a self-intersecting solid, and one touching it with UnsupportedInput instead of compiling a degenerate solid, for full and partial turns alike (#351, from axiolid-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 (was MPL-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 to 4e-10 m off their plane read as exact. Coplanarity is now decided in exact arithmetic on the stored corners (certified orient3d), and a face that fails it reports Certified(w) under "non-planar face without a surface", the width #261 computes, however small; 0 now means planar. An exactly coplanar face, tilted or not, still reports 0; 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-15 and the slit that opens, about 1e-13 wide, is returned closed: both stay refused. tests/half_space_chain_contact.rs pins the chains and the notch; probe scripts/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 within 1e-8 was 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 wall 5 x 0.25 x 3 less a door and windows whose caps and shared side are 1e-8 and 3e-8 off is certified at about its readings (2e-8, 6e-8), its exact volume checked; at 0 it stays at rounding (1.3e-12). Probe scripts/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-reference 0.3.9, and through it axiolid-evaluate 0.3.17, whose certified frame laws and intrinsic positions 0.3.28 uses. 0.3.28 accepted axiolid-reference 0.3.8, which accepts any axiolid-evaluate from 0.3.12, so a lockfile holding an older one did not build.

0.3.28 - 2026-10-18 ​

Added ​

  • PolylineCompiler compiles trimmed, composite, offset and intrinsic curves (#317), which it refused by name, so an OpenProfile whose 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 by h^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 not C^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 from axiolid-brep-boolean with 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 (the station::bound module 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 by MITRED_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 stay UNCERTIFIED_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 IfcLine with 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 from Edge::start to Edge::end: an exchange format's edge that runs against its curve (IFC SameSense = .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 radii 6 to 6.25 less five wedge openings, abutting by 15 um and 0.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's z, at any angle to the plane, the boundary at placement * (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, whose z leans 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.8 to 5 less a niche of 10 degrees reports 0.99973 mm at a 1 mm request, as the niche placed well inside does, with the coincidence reported (at most 4e-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 y from 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 whose z points 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 ​

  • ReferenceMeshCompiler implements PolylineCompiler (#311): a Curve3, a Curve2 (lifted to z = 0), an OpenProfile and an Instance of any of them, or an InstanceAtStation on a line basis, compile to a 3D polyline. Each smooth piece of the curve (cut where it may not be C^1) is bisected until every span's certified chord bound, the exact sagitta of a circular arc or h^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_mesh still refuses curve nodes with CurveEvaluation.
  • Surfaces trimmed by a parameter rectangle (SurfaceRelation::RectangularTrimmed over a plane, cylinder, elliptical cylinder, cone, sphere, torus or B-spline surface) compile through compile_mesh as sheets reported MeshClosure::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 within 1/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_deviation reports it under DeviationPath::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 atomic Surface node 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_EPSILONS and deviation::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 (IfcEdgeCurve without an IfcPcurve), 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 by axiolid-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; check meets_requested. The crate now depends on axiolid-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.67 less two doors reported 9.96e-4 at a 1e-3 request for a mesh whose certified volume is exact to 1e-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 reports 1.18e-12 (sampled 3.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 f64 transform (#295, ADR 0080 amendment). An instance moved its source's vertices by a few ulps of their coordinates, about 1e-9 at 6e6, 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 its Proven or Certified bound. Every bound of an Instance or InstanceAtStation now adds 4 f64::EPSILON M, M the largest magnitude a placed coordinate sums (derived in deviation::placement_rounding; an identity or a mirror adds nothing), and names it under DeviationPath::Placement. The source is meshed with room for it (the floor at the translation's scale plus a 2^-20 share 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 with BUDGET_BELOW_PLACEMENT_ROUNDING. A torus at (6e6, 6e6, 100) with a 1e-9 budget is refused; with 2e-8 it reports within the budget, about a quarter of it the rounding, and dense samples of the exact torus against the f64 mesh stay within the bound. Instanced bounds are now up to a 2^-20 share below the budget rather than at it. compile_polyline through an Instance or InstanceAtStation follows the same rule (term, room, refusal): a polyline placed at 6e6 reports 4 eps (6e6 + 4) where it reported 0.

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, through axiolid-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::EPSILON of the operands' own coordinates (about 1.3e-14 for a 3.67 m wall), so doors 2e-14 to 1e-10 short of a wall's face left a skin that thin, and once an Instance placed the wall 5600 km out its faces crossed and enclosed_volume refused the mesh as self-intersecting. The reach now covers the coordinates the result is placed at: Instance and InstanceAtStation carry the placed coordinate scale down to their source with the tolerance and the chord budget, and the snap reaches 16 f64::EPSILON of it (about 2e-8 there), 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 under deviation::SNAPPED_OPERANDS as 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_deviation measures 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 reports DeviationPath::Boolean under "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, so meets_requested holds 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"); ReferenceExactCompiler itself still refuses mesh operands by name.
  • Certified deviation for runs of sections at stations (#247, ADR 0082 amendment): StationedSpine, SectionsAtStations, SectionedSurface and OpenSectionsAtStations report DeviationBound::Certified instead of Unbounded wherever 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 bound h^2 / 8 sup |A''|, breakpoints at the seams inside it, the wall quad's loft::quad_deviation and 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.
  • ReferenceExactCompiler compiles a StationedSpine or SectionsAtStations exactly 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 by PathBound::Point at those points, with and against its sense. A point off the B-spline, or one it passes through twice, is refused naming PATH_POINT_OFF_CURVE or PATH_POINT_AMBIGUOUS unless 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_face over construct's triangulate_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 as Solid rather than OpenSolid and 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" (path BRepFace or AuthoredMesh), 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.0 meets its 0.0 twin, 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), reported Certified under the new detail deviation::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 OffsetsAtStations at its basis's start (RunStation::at_basis_start) is read at 0, one at its end (RunStation::at_basis_end) at the basis's length by the kernel's own measure, the length station::curve_path states for the basis as CompositeBasis measures 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 as InvalidInput.

0.3.22 - 2026-10-12 ​

Added ​

  • Run stations snap onto seams within their windows (#301, ADR 0082 amendment): SectionsAtStations and OpenSectionsAtStations sections and the stations of the new CurveRelation::OffsetsAtStations (compiled as a sweep directrix and as a station basis, like OffsetByStations) 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 as InvalidInput. 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. Window 0 and Outgoing, the defaults, are today's reading.

Changed ​

  • station::curve_path documents 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_requested false (#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 at 1e-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 its 1 + 96 eps margin, since an f64 rotation 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::resolve and station::placement, and so every InstanceAtStation compiled or used as a station basis, read a station within its window of exactly one seam of its basis on that seam, from its SeamSide (its point the seam's), through axiolid_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 of 0, 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 neutral axiolid_curve::CurvePath a station on it is measured along, for a consumer to read through the curve-evaluation contract's path_* 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_path and every run of stations (spines, sectioned surfaces, offsets by stations) accept a constant CurveRelation::Offset (2D without, 3D with a reference direction) and an OffsetByStations as 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::EPSILON of the operands' largest coordinate magnitude (about 1.3e-14 for a 4 m wall, about 2e-8 at 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. A 1e-12-1e-9 skin 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 as a + (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 x axis 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). ReferenceExactCompiler clips 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. ReferenceMeshCompiler flattens 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, as InvalidInput naming 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::placement and 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 one axiolid_reference::station::CompositeBasis. A station on a joint reads the piece its SeamSide names; 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::seams reads 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 the DeviationPath::Boolean contribution that reports operands snapped together within the tolerance (see Fixed), its Certified bound 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, an EllipticalCylinder along the direction, where ReferenceExactCompiler returned 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 stays DeviationBound::Unbounded under 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: ReferenceMeshCompiler refuses an extrusion whose direction lies within tolerance of its profile plane, as "extrusion direction in the profile plane" (#281, through axiolid-construct's extrude_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, with a 1 um off p1-b, ended up in no triangle of the cap while the side faces kept it, and a closed faceted prism meshed OpenSolid. 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-15 apart, such a skin crossed itself once the mesh was placed at georeferenced coordinates, and the volume kernel refused it as self-intersecting. ReferenceMeshCompiler now 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::placement resolves a GeometryNode::InstanceAtStation to a ResolvedPlacement { transform, station, exact } -- its oriented station resolved as station::resolve does, seam side included, and the rigid motion taking the source's local x, y, z onto the oriented tangent, left lateral and up. ReferenceMeshCompiler meshes a placed solid or surface like an instance; on a basis other than a line the placement is reported DeviationBound::Unbounded under the new DeviationPath::StationPlacement (appended last). ReferenceExactCompiler places 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 own z = 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, an OrientedCurveStation and 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::seams too). A placement whose frame tilts +Z is 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::resolve reads an OrientedCurveStation on a seam of its basis from the piece its seam names (a plain CurveStation the 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). ReferenceExactCompiler refused 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", so compile_mesh_with_deviation left every boolean with such an operand unbounded. It now compiles them through axiolid-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 -z clipped 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, in axiolid-mesh-compile-contract) unless every edge is shared by exactly two triangles running it in opposite directions; solid_mesh refuses 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 IfcPolygonalFaceSet exporters 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 under PinchPolicy::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.0 corner 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, and axiolid-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) or Curve3::Banked directrix, 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 as Unsupported { 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 +-h reports 2h (was h); a square with one corner lifted by h reports h/2, the gap between its two triangulations at the centre (was h/4). Still DeviationBound::Certified under "non-planar authored face", never Proven; 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::Certified under the detail "non-planar face without a surface", and DeviationReport::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 the Proven(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 under PinchPolicy::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::AreaMismatch is now PolygonRefusal::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. earcut is 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 reported Proven(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) and scripts/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::resolve also resolves an OrientedCurveStation, its frame turned by the orientation, its point and section not. The new SectionsAtStations spine and OpenSectionsAtStations sheet 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 CurveRelation boundaries (#255): a Composite of lines, polylines, arcs and other segments, each read in its same_sense, and a Trimmed basis curve under every TrimSelector kind (parameter, point, arc length), resolved to points by the sweep directrix reader, 2D curves and point selectors lifted to z = 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, others Unbounded by name) plus its widest joint gap. A joint or closing gap no wider than its ends' own rounding (8 machine 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 and meets_requested holds.

  • Stations (#241, ADR 0082). station::resolve turns a CurveStation node into its point and frame (x tangent, y up, z right). The reference mesh compiler meshes SolidOperation::StationedSpine as a closed loft and SurfaceRelation::SectionedSurface as 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; an OffsetByStations curve is sampled as a sweep directrix (its parameter is the basis distance). Their deviation is reported Unbounded by name (DeviationPath::StationedSpine, DeviationPath::SectionedSurface), and ReferenceExactCompiler refuses 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_deviation reports it as AuthoredMesh with the detail "non-planar authored face", always Certified, never Proven: 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 ​

  • ReferenceExactCompiler compiles a round web hole touching the flange of an I-beam with root fillets (#249): exactly, with an empty report, at Tolerance::ZERO with exact axes, and reading the contact under a general placement. Its mesh is measured against the exact result through compile_mesh_with_deviation and reported Certified (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").

  • ReferenceExactCompiler compiles 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, at Tolerance::ZERO too; otherwise it carries PlaneTouchesCylinder. Through compile_mesh_with_deviation such 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 SweptDisk whose 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, as IfcSweptDiskSolid defines 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_deviation reports such pipes Proven at 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, now UnsupportedInput (its closing mitre is not built). The exact compiler still refuses directrices with corners.

Added ​

  • ReferenceExactCompiler::compile_exact_with_report and compile_exact_batch_with_reports carry 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_FACTOR is re-exported.

0.3.11 - 2026-10-03 ​

Added ​

  • Sweep directrices resolve TrimSelector::ArcLength (#239) against an analytic basis, in the trim's sense, through axiolid_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 ​

  • ReferenceExactCompiler clips by half-spaces exactly (#234): a difference or intersection whose tool is a HalfSpace, or a BoundedHalfSpace (a polyline boundary in the plane), read through rigid placements, with the mesh compiler's semantics (agreement selects 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 is Degenerate.
  • ReferenceExactCompiler::compile_exact_with_report and compile_exact_batch_with_reports return each body with a BooleanReport (re-exported from axiolid-brep-boolean, with ToleranceDecision and ToleranceDecisionKind) 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 (entries 0 and +-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_deviation reports a boolean Certified where ReferenceExactCompiler builds 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 stays Unbounded, 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 ​

  • ReferenceExactCompiler compiles 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 +z keep 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 is Degenerate. 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_deviation and DeviationReport (#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: the MeshCompiler contract is unchanged.

Changed ​

  • A swept disk's fillet_radius is 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_directrix and ExactDirectrix are public (#230): they read a swept disk's directrix as one segment or one arc, exactly as ReferenceExactCompiler does, 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::revolve for the proof), and its turn follows ExecutionOptions::with_chord_error instead 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 with BudgetExceeded rather 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 ​

  • ReferenceExactCompiler compiles Instance nodes (#223): the source's exact B-rep is placed with ExactBRep::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.
  • ReferenceExactCompiler compiles SweptDisk exactly 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::Degenerate naming 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 IfcCurveBoundedPlane space-boundary connection surfaces carry them: a SurfaceRelation::CurveBounded over a planar basis compiles to a triangulated planar region with MeshClosure::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 and implicit_outer get UnsupportedInput naming 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 at Tolerance::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 PolygonMesh and 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 at Tolerance::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 of audit_mesh, which dropped real faces below its area threshold before counting edges. On two real ArchiCAD models this turns 505 authored-closed IfcPolygonalFaceSet products from Surface into Solid; 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 start the way sense_agreement says, 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 (a 315 -> 45 degree bend swept the 270 degree arc); inside a composite the ends no longer met and the sweep was refused as composite directrix has a N unit gap. In a real Revit rebar model that was 1,494 bent bars, all writing a bend as 270 -> 45, 270 -> 15 or 270 -> 360 rounded just past the seam. A full turn rounded past the seam stays a full turn. A sweep parameter_range on such a trim is read in the trim's unwrapped interval, so (330, 30) and (330, 390) degrees name the same sub-arc of a 315 -> 45 trim; an end off the arc is refused as before. Profiles already honoured this.

0.3.1 - 2026-09-24 ​

Added ​

  • PolygonMesh faces that are not plain triangles compile (#160): n-gons, concave faces and faces with holes (IFC4 IfcIndexedPolygonalFaceWithVoids) 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::Surface through compile_mesh_reported, even when the shell is closed. Collections are Solid only if every member is, and a boolean with a surface operand is refused. Authored meshes report Solid exactly when they are closed, consistently wound two-manifolds.

Changed ​

  • A PolygonMesh with non-triangular faces used to fail with GeomError::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 ​

  • Instance and Collection nodes no longer drop attribute channels and normals (#115). Both used to rebuild the mesh from positions and indices only, so a textured item lost its uv channel as soon as a product had a second item or was instanced — silently.
    • Instance carries 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.
    • Collection merges channels by name. A channel on only some members becomes corner-indexed, with the other members' triangles UNMAPPED; a channel on every member as per-vertex stays per-vertex. Members defining one name with a different width or blend drop it as DropReason::IncompatibleChannels.
    • Booleans keep the provider's channel fates, composed onto what each operand already went through, instead of discarding the evidence.

Added ​

  • ReferenceMeshCompiler implements MeshCompiler::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 LICENSE and LICENSE-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 (was MPL-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 of compile_mesh, whose signature stays as it is. PolylineOutcome carries the vertices, whether the curve closes, and a ChordDeviation reported 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, with meets_requested. The bound is never absent: a curve a compiler cannot bound is refused by name.
  • A direct dependency on axiolid-core for Point3 and Scalar; it was already in this crate's closure through axiolid-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 claiming Solid for a mesh with boundary edges; CompileOutcome::solid_mesh refuses it by name. MeshClosure is #[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 ​

  • MeshClosure and CompileOutcome::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 for untracked and tracked, so existing compilers build unchanged). CompileOutcome::solid_mesh returns the mesh only for Solid, so volume readers refuse a surface model instead of measuring a closed shell the source never declared a solid. with_closure sets it.

0.3.0 - 2026-09-23 ​

Added ​

  • CompileOutcome and the provided method MeshCompiler::compile_mesh_reported (#115): a compiled mesh with the fate of each attribute channel. The default wraps compile_mesh and reports attribute_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 LICENSE and LICENSE-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 (was MPL-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 new PerspectiveConformanceFailure::HiddenLines. This replaces the 0.1.5 behaviour in which every claim failed closed with ClaimUnverifiable; a visible-face claim still does until its fixtures land (ADR 0107, #387).

0.1.5 - 2026-11-13 ​

Added ​

  • FaceGroups::Planar and HiddenLineMesh::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::GroupBoundary and EdgeSelection::group_boundary (default false) with EdgeSelection::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, and Perspective), the View::projection field, View::perspective(frame, near) (refuses a near that is not finite and below zero) and a defaulted supports_projection on MeshHiddenLines and MeshVisibleFaces that is true for Orthographic only. Registries route by it, so a provider that predates the field is never given a perspective view. The new conformance::perspective::PerspectiveConformanceSuite infers the claim from supports_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 ​

  • OrthographicView is renamed View, 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_regions and collapsed_holes (#410, ADR 0100): how far snap rounding moved region boundaries, as a DisplacementBound, and how many edges it rerouted and regions and holes it collapsed. input_mesh starts them at zero. The conformance suite requires zero in every exact case.

Changed ​

  • MeshVisibleFaces regions 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; and VisibleRegion::triangles lists 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 MeshVisibleFaces operation contract (ADR 0094, #383): visible face regions of meshes in the same orthographic view, with FaceGroups per triangle, FaceSource (group or near-plane cap), VisibleFaceRequest, VisibleFaceLimits, VisibleRegion, evidence and FACES_CAPABILITY_ID.
  • conformance::faces::FaceConformanceSuite for MeshVisibleFaces providers: 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::groups and HiddenLineMesh::with_groups; the field is read by MeshVisibleFaces only.

0.1.2 - 2026-11-03 ​

Added ​

  • The package now ships LICENSE and LICENSE-COMMERCIAL (symlinks to the repository root), so the licence texts travel with the archive (#358).
  • RefusedMesh: a refused input mesh and its GeomError, 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 MeshHiddenLines operation 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 (a GeomError plus 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 LICENSE and LICENSE-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_crossings and with_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 Both regions as cut regions.

Changed ​

  • Every InPlaneSide::Both region 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 a Both region. A provider that reports a Both region 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_pairs and RegionLimits::max_in_plane_pairs count 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 a max_in_plane_pairs sized to the old counts can now give BudgetExceeded.
  • 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) and PlaneSection::cut_regions: the cut of a solid, where its material lies on both sides of the plane, as closed CutRegions (outer ring, holes) whose rings run through the chain and region vertices bit for bit. Refusals REFUSED_PINCHED_CUT_REGION and REFUSED_CROSSING_CUT_RINGS; evidence fields cut_regions, cut_region_vertices and cut_pairs. Additive (ADR 0092, #372).
  • RegionConformanceSuite runs 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 reports cut_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) and SectionRequest::role with with_role: a sheet is an open surface (terrain, a surface body), sectioned with boundary edges allowed. ChainEnd::MeshBoundary and the evidence fields role and boundary_edges (PlaneSectionEvidence::with_role). The default stays Solid, today's semantics. Additive (ADR 0091, #359).
  • RegionConformanceSuite adds 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_claimed is false when the provider refuses the first sheet case with Unsupported; the sheet cases are then skipped.

0.3.3 - 2026-10-28 ​

Added ​

  • SectionRequest, PlaneSection and the provided MeshPlaneSection methods section_request and request_scratch_requirement: faces lying in the cutting plane come back as InPlaneRegions with an InPlaneSide, cut curves are transversal SectionChains (open ones end on a region), and touching edges are counted in PlaneSectionEvidence. A provider that does not implement the method refuses with Unsupported. The refusal names are REFUSED_OVERLAPPING_IN_PLANE_FACES, REFUSED_IN_PLANE_FACES_ON_BOTH_SIDES and REFUSED_SADDLE_VERTEX. Additive (ADR 0091, #359).
  • conformance::RegionConformanceSuite runs 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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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::Revolution and LinearExtrusion document their parameterisation: a revolution is O + R_a(u) (c(v) - O) with u the right-handed angle about the normalised axis and v the swept curve's own parameter, an extrusion is c(u) + v d with v in units of the direction's length (#411, ADR 0103).

0.3.18 - 2026-11-03 ​

Added ​

  • SurfaceRelation::Ruled { first, second } and RuledSide: a surface ruled between two sides, p(u, v) = (1 - v) first(u) + v second(u). A side is an atomic Curve3 node 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 new GraphError::InvalidRuledSurface { detail }; a side that is no atomic Curve3 is an InvalidReferenceType (#402).
  • Wire format 1.5: only a graph holding a Ruled surface is labelled 1.5; every other graph is written as before, and a 1.4 payload holding one is refused as UnknownKind (#402).
  • The package now ships LICENSE and LICENSE-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::FixedReferenceSweep states the frame law, the tangent-continuity requirement, how parameter_range is 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 (was MPL-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 with BoundedHalfSpace's fields: the kept side of the plane within the prism of the boundary along the placement's z, infinite both ways and at any angle to the plane, the boundary at placement * (u, v, 0) as written. This is how IFC's IfcPolygonalBoundedHalfSpace bounds its half-space, by its Position. BoundedHalfSpace keeps its fields and meaning. The builder validates the new variant's references as the old one's. SolidOperation is #[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. Golden tests/wire/v1.4/every-kind.json.

Changed ​

  • BoundedHalfSpace.placement is documented as read whole (#322): its x projected 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_json was used without its float_roundtrip feature, 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): RunStation gains basis_end: Option<BasisEnd> (a new #[non_exhaustive] enum, Start or End), set by the constructors RunStation::at_basis_start(offsets) and RunStation::at_basis_end(offsets); None, the default, is the stated station.distance as before. Such a station states the distance 0, which is not read; a resolver reads the start as 0 and the end as the basis's length by the kernel's own measure. The graph refuses, as GraphError::InvalidStation when 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 than 0; 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 as UnknownField basis_end, by the same test the writer labels by. Golden payload tests/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): SectionAtStation gains seam and seam_window, set by with_seam_side and with_seam_window, for SectionsAtStations and OpenSectionsAtStations; a new RunStation (#[non_exhaustive], built by new or From<Station> and the same two methods) carries them in a new variant, CurveRelation::OffsetsAtStations, the general form of OffsetByStations. Defaults are Outgoing and 0, today's reading; Station, StationedSection, StationedOpenSection and the #241 variants are unchanged. A negative or non-finite window on any run station is refused as GraphError::InvalidStation when the node is pushed.
  • Wire format 1.2 (ADR 0085 note): the two fields are written only when they are not Outgoing and +0.0, and OffsetsAtStations is 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 (UnknownField seam or seam_window, UnknownKind OffsetsAtStations), by the same test the writer labels by. Golden payload tests/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 payload 1.1, which such a reader refuses. A graph holding a window anywhere (an OrientedCurveStation node or the station of an InstanceAtStation, a window of -0.0 included, since it is written) is written as 1.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 by with_seam_window, a non-negative distance in the basis's station measure (default 0, 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 its SeamSide, and refuses a window holding two seams or a seam whose certified position straddles its edge. It also applies to the station of an InstanceAtStation. A negative or non-finite window is refused as GraphError::InvalidStation when the node is pushed.
  • Wire format 1.1 (ADR 0085): the window is written as seam_window and omitted at +0.0, so a graph without windows writes the 1.0 graph; an absent window reads as 0, so every 1.0 payload still reads to the same graph. A payload claiming 1.0 that holds a window is refused as WireError::UnknownField. FORMAT_VERSION is 1.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 GeometryGraph behind an optional serde feature, 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_cbor and from_cbor, and the wire module with FORMAT_NAME, FORMAT_VERSION, FormatVersion and the typed WireError. 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 through GeometryGraphBuilder::push, so it passes the same validation as any other. A writer refuses a graph holding a non-finite number. Golden payloads under tests/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 as WireError::Malformed naming 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 serde feature, every node payload type derives Serialize and Deserialize. A NodeId deserialises only inside a graph payload.

0.3.7 - 2026-10-09 ​

Changed ​

  • A SolidOperation::BoundedHalfSpace boundary may be a Profile node 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 station module 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 by InstanceAtStation::new) and the node variant GeometryNode::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 local x, y, z map onto the oriented tangent, left lateral and up, its origin onto the station's point (a linear placement's reading), as the station module 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): OrientedCurveStation gains seam: SeamSide (re-exported from axiolid-curve), the piece a station on a seam of its basis reads; OrientedCurveStation::new sets SeamSide::Outgoing, the stated default and the evaluators' behaviour, and with_seam_side changes it. CurveStation has public fields, so it keeps reading the outgoing piece; CurveStation::with_seam_side returns the oriented station in its base frame reading the given side, and From<CurveStation> for OrientedCurveStation the unturned outgoing one. The station module 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_axes fills the defaults (0, 0, 1) and (1, 0, 0). ORIENTATION_TOLERANCE bounds 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 by new, with_tags, with_orientation; it is #[non_exhaustive], so fields can follow additively) and its two relations, SolidOperation::SectionsAtStations (closed profiles, the general form of StationedSpine, matched by tag when tagged) and SurfaceRelation::OpenSectionsAtStations (open sections, the general form of SectionedSurface). Both variants are appended to their enums. From<StationedSection> and From<StationedOpenSection> convert the #241 sections.

  • GraphError::InvalidStation also 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 and StationOffsets lateral/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 upright StationFrame::Plan). GeometryNode::CurveStation(CurveStation) is a point and frame at a station; CurveRelation::OffsetByStations a 3D curve through offsets at stations, interpolated linearly in distance; SolidOperation::StationedSpine closed profiles standing at stations (StationedSection), matched by ring and vertex index; SurfaceRelation::SectionedSurface open sections at stations (StationedOpenSection) joined by tag.

  • GraphError::InvalidStation names 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 SectionedSurface whose 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 parameter 0 in 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 satisfies TrimmingPreference::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::BoundedHalfSpace boundary 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::QuadraticGraph or Curve2::AngleGraph with finite coefficients is accepted as a trim basis (#119).

  • A Curve2::Sinusoid is 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 LICENSE and LICENSE-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 (was MPL-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 as Status::BudgetExhausted and refused by converged(), 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 as Unresolved regions with the most roots they can hold.
  • integrate: adaptive Gauss-Kronrod (G7/K15) with QUADPACK's error estimate and a panel budget; GaussLegendre fixed rules up to 512 points.
  • Matrix, Lu, Cholesky and column-pivoted Qr, with Hager-Higham condition estimates and forward error estimates; least_squares and constrained_least_squares (null-space method).
  • levenberg_marquardt over a Residuals trait (analytic or forward-difference Jacobian), and minimize_scalar (Brent's localmin). Both report a Status; 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 return Undecided instead 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 LICENSE and LICENSE-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_certified and invert_periodic_surface_certified refuse an off-surface point as OffSurface once the certified lower bound exceeds the tolerance, where they returned BudgetExceeded from refining the retained boxes down to the parameter resolution (#320). Projection is unchanged.

0.3.6 - 2026-10-10 ​

Added ​

  • exact_surface_intersection cuts 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_curve3 and fit_curve3_to_tolerance: least-squares B-spline curve approximation to scattered points (#150, ledger B11), distinct from interpolate_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 through axiolid-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_tolerance adds control points until the max deviation meets the caller's tolerance, refusing with GeomError::BudgetExceeded rather 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 along u sharing one design matrix, then each column of the result along v, 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 f64 cannot (#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 as Undecided until 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/_v remove 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/_v raise a degree exactly. reduce_surface_degree_u/_v lower one within a tolerance (BoundedSurface), or refuse; rational surfaces are refused, as for curves. iso_curve_at_u/_v extract 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 f64 rounding 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 to x^21), their derivatives tried up to that degree. Series fields keep f64, 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 were NotRegularCurve, 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::value evaluates 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, as PairSection3 curves. 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_intersection returns it for two B-splines (Derivation::PairTrace).

  • exact_curve_surface_intersection for 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_intersection and trace_section_pcurves take a B-spline carrier.

  • exact_curve_surface_intersection takes 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_intersection takes an ImplicitSection, and section_curve_surface_intersection takes any ruled, torus or traced section over a span. Roots are isolated along the curve's cells with interval bounds. Parameters are ExactCurveParameter::Certified.

  • trace_section_pcurves, extract_stretch and implicit_view give 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_intersection now 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 as NotRegularCurve. implicit_surface_intersection exposes it with an explicit window for any analytic pair, and section_field_of the field itself.

  • exact_surface_intersection derives 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. ExactIntersectionCurve gains spans and Derivation gains RuledQuadricSection.

  • exact_surface_intersection derives 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 -- as u(v) solving A(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) and exact_curve_curve_intersection2/3 (lines, circles, ellipses). The equations become integer polynomials in the line parameter or the conic's half-angle parameter, solved with axiolid-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 as Antipode. A cone counts only its modelled nappe; a line on the cone through its apex is refused as PartialOverlap. Points are rounded to f64 for output only. B-spline operands stay on the certified tier.

Fixed ​

  • exact_surface_intersection decides tangent, parallel and perpendicular cases exactly (#119). In f64 a 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 (with locate), FreeSpaceInput, FreeFace, FreeRing, Bridge, PieceSource, FaceLocation, FreeFaceEvidence, FreeFacesError and BarrierDefect (#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 on axiolid-exact-arrangement now.

0.4.0 - 2026-11-13 ​

Changed ​

  • Breaking: OverlayError is #[non_exhaustive] and gains HolesOverlap and PolygonsOverlap, which Region::new returns; a match on it needs a wildcard arm. HoleOutsideOuter now also names a hole that crosses its outer ring. Region::translate checks its result as Region::new does, since rounding each vertex can turn a contact into a crossing; Region::sweep moves its copy unchecked, since the copy only feeds union, and returns what it did before (#448, ADR 0104).

Fixed ​

  • MorphologyMode::Fast fills 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 than t: #340's room with a column and 124 such needles gave 562 corners, 500 needles 2,066, both as Exact. Proposals now also pass t over a needle no wider than needle_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 than t is 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 of benches/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 and Exact are 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.py lists the faults its tests must catch.

  • Region::new refuses 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::new of 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, case holes). overlay and union_soup operands are unchanged. src/nesting.rs; scripts/probe_region_nesting_mutants.py lists 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, cases corridor and bays, #448).

  • MorphologyMode::Fast fills 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, Exact is 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, walls floor; docs/research/fast-morphology-on-toothed-floors.md; src/fill.rs, #345, ADR 0087). scripts/probe_fast_morphology_mutants.py lists the faults its tests must catch.

0.3.22 - 2026-11-03 ​

Added ​

  • check_arc_polygon_with and ArcCheckOptions::with_clearance add 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 an ArcPolygonDefect::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 with undecided set, never cleared. A non-finite or negative distance is an ArcPolygonDefect::InvalidClearance. check_arc_polygon and the default options are unchanged (src/arc_clearance.rs, #388, ADR 0095). scripts/probe_arc_clearance_mutants.py lists the faults its tests must catch.
  • OverlayError implements Display and std::error::Error, so it formats without {:?} and converts into Box<dyn Error> (#354).

Changed ​

  • OverlayRefusal's message uses the wrapped OverlayError's own wording, and Error::source returns it (#354).

Fixed ​

  • MorphologyMode::Fast fills the notches between needles. Its chord proposals reached only t / 16 from the chord's line (0.0125 m for a disc of radius 0.4 m, t 0.2 m), so a wall of needles 0.05 m deep kept every notch and the sum kept its quadratic cost. Proposals now reach t. A notch whose spanned vertices all project onto its chord, of length g, also has the excess sqrt(t^2 + g^2 / 4) - t, which joins the least of its depth and its gap bound; proved in src/fill.rs. Fast still keeps a needle deeper than t: 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.py lists the faults its tests must catch.

0.3.21 - 2026-11-03 ​

Added ​

  • The package now ships LICENSE and LICENSE-COMMERCIAL (symlinks to the repository root), so the licence texts travel with the archive (#358).
  • check_arc_polygon decides 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 (was MPL-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_within and union_soup_within take OverlayOptions and the caller's ExecutionOptions (#338, ADR 0088): the tolerance is the options', and with ExecutionOptions::with_memory_budget the subdivision charges what it is about to hold before allocating it and is refused with OverlayRefusal::BudgetExceeded { needed, budget } as soon as the charges pass the budget. OverlayRefusal::Invalid carries every refusal overlay_with and union_soup_with make. Without a budget nothing new is refused, and an admitted call gives the unbudgeted result bit for bit. tests/grown_soup.rs measures 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 on axiolid-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_intersect decides 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 a Region carries the proof that its rings are valid under the tolerance it was built with: Region::new sets it, and a boolean on regions and the disc morphology set it from their settled output, so Region::union, intersection and difference under 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 of Region equality. Measured with cargo bench -p axiolid-overlay --bench ring_validation (release), on a 40 m by 30 m floor with toothed walls: Region::new at 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; a Region::difference with 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.py lists 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_soup at 128 facets and 1.9 GB and 10 s for overlay(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): circumscribed union_soup at 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 in tests/grown_soup.rs. scripts/probe_grown_soup_mutants.py lists 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_with and dilate_outer_with take a MorphologyMode:

    • MorphologyMode::Exact, the default, is what erode_inner and 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 within budget, in metres; a refused chord is split into shorter ones. The result keeps its inner or outer side. The excess is part of MorphologyBound::deviation, and Region::excess reports 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.py lists 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::ZeroArea footprints 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 of dilate_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.rs pins 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.py lists 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 with cargo 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.rs pins the toothed room's erosions against 0.3.15 by hash, a unit test compares every route with the untrimmed hulls bit for bit, and scripts/probe_minkowski_cover_mutants.py lists 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 k slender 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 at k = 100, 200 and 400 and now takes 0.23 s, 0.94 s and 3.9 s, growing as the crossings do (about k^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 with cargo bench -p axiolid-benchmark --bench overlay_slender, interleaved runs against 0.3.14 on a shared machine. The time had grown about k^2.65, because the turned edges' boxes hold much of the soup and every piece of the subdivision searched O(k) of them for its carriers and counted O(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 of exact_overlay give 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.rs pins the outputs on the consumer's soups, a torus, a cylinder, random triangles and an arc subdivision against 0.3.14 by hash, and scripts/probe_overlay_slender_mutants.py lists the faults it and the suite must catch.

Fixed ​

  • The crate's internal exact booleans refuse a ring with a repeated vertex as RepeatedVertex instead 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, and scripts/probe_repeated_vertex_mutants.py lists 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_with and union_soup_with take OverlayOptions; their default is overlay and union_soup exactly, 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 most tolerance.linear()^2 under the exact ZeroArea rule, and lists it in ReportedOverlay::dropped / ReportedUnion::dropped as a DroppedRing: 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 a RepeatedVertex ring 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.py lists the faults tests/degenerate_drop.rs must 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 of x, which made mesh soups slow. Arc booleans share the changes and are unchanged or faster in benches/arc_overlay. tests/soup_membership.rs checks soups by point membership, and scripts/probe_overlay_soup_mutants.py lists the faults it and the suite must catch.

0.3.13 - 2026-10-10 ​

Fixed ​

  • overlay accepts 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 with 0.0 counted as a side: two disjoint edges on one line whose cross products rounded to 0.0 and to a tiny positive residue counted as crossing, and the union of a turned U fed back was refused as SelfIntersection (6 of 20,000 turned copies in the report). Every contact decision is now exact, in the new contact module 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.py lists 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 ArcArrangement kept 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::regions fills 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 ZeroArea check 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 as ZeroArea, and a clockwise sliver of a chorded round corner read as counter-clockwise made the union of two triangles sharing an edge fail with SelfIntersection. 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, and validate_arc_ring for 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 orientation arc_overlay and ArcArrangement give their operands. A ring now decides the same at any distance from the origin. arc_ring_area, ring_area, polygon_area and Region::area sum 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 at x = 4.2 and was refused at x = 4. A ring with arcs, or one passed to validate_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 as 3.9999999999999996..4.199999999999999 between 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_sum and Region::minkowski_erosion now take any simple polygon, where a non-convex one was refused with MinkowskiError::NotConvex. New Region::minkowski_sum_region and Region::minkowski_erosion_region take 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_erosion by 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_intersections reports 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, in O((n + k) log n) time for k reported 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 are ExactPoint2 rationals, 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_soup and Region no longer go through i_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 area 0.8, not 0.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, and CircleEvidence::error bounds 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_soup settles its output like every other operation, so its polygons are accepted by Region::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 an OrientedRectangle (centre, unit axes, half extents) and RectangleEvidence (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 a RectangleError.
  • 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::new accepts 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 as RepeatedVertex or SelfIntersection. 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_sum and Region::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 one ArcArrangement with 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 as MinkowskiError::NotConvex.

  • Region::dilate_inner, dilate_outer, erode_inner and erode_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::bound gives the side (BoundSide::Inner or Outer) 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. dilate and erode keep 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 as arc_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, and regions(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 ​

  • ArcArrangement skips 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_overlay is 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, via axiolid-exact; none reads the tolerance. Crossing points of two curves are rounded to f64 once, in the output, and edges shorter than the tolerance after rounding are merged. The public API is unchanged.
  • cavalier_contours is 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_overlay skips 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 ​

  • overlay no longer rejects a U-shape or comb as SelfIntersection. 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_overlay results 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 LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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 ​

  • incircle and insphere returned 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, as orient3d does.

    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 accumulator orient2d and orient3d share), which forms no difference and no rounded product. Cost, measured with the committed predicates bench 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.

  • StaticFilter::orient3d certified 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::new now returns None when 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 dynamic orient2d filter declines more (outside [1e-90, 1e90]) since #404.

  • orient2d returned 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, as orient3d does.

    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_filter declines differences outside [1e-90, 1e90] as Uncertain. 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 committed predicates bench. Non-degenerate input is unaffected (about 15 ns).

Added ​

  • The package now ships LICENSE and LICENSE-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 (was MPL-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_sphere and in_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; Uncertain outside the exactly evaluable range (non-zero coordinates beyond [2^-100, 2^100]).

Fixed ​

  • insphere_filter bounded 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 tries i128 arithmetic 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 ​

  • incircle and insphere: the exact fallback rounded the coordinate differences to f64 before its exact expansion arithmetic, so on nearly cocircular (cospherical) points whose differences do not fit an f64 -- 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 LICENSE and LICENSE-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 (was MPL-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 serde feature, off by default, deriving Serialize and Deserialize for Primitive and HalfSpace: 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's MakeWedge general 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 LICENSE and LICENSE-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 (was MPL-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 serde feature, off by default, deriving Serialize and Deserialize for Profile and 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-overlay 0.4, whose OverlayError is #[non_exhaustive] with the new HolesOverlap and PolygonsOverlap, and whose Region::new refuses rings that cross into each other. ProjectionError::Planar carries that OverlayError, so a match on it needs a wildcard arm (#448, ADR 0104).

0.3.3 - 2026-11-07 ​

Added ​

  • The package now ships LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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::TriangleIndexOutOfRange instead of being skipped by nearest_hit_among; a broad phase built over a different mesh would otherwise report "no hit" for triangles it never tested. triangle_hit refuses 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::ChainPositionTable and bound::SeamSide are re-exported from axiolid-evaluate (#438): certified points, tangents and curvature vectors of an arc-length chain. bound::ElevatedPositionTable takes a chain plan and reads its tangent and second derivative (jet3_on, jet_errors).

0.3.17 - 2026-11-13 ​

Added ​

  • bound::ElevatedPositionTable is re-exported from axiolid-evaluate 0.3.21 (#393): certified points of an elevated or banked curve over an intrinsic plan, which axiolid-mesh-compile reads for its swept disks.

Changed ​

  • Requires axiolid-evaluate 0.3.21.
  • section_request with 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); a Both ring that repeats a vertex stays refused.

0.3.16 - 2026-11-03 ​

Added ​

  • The package now ships LICENSE and LICENSE-COMMERCIAL (symlinks to the repository root), so the licence texts travel with the archive (#358).

Changed ​

  • Requires axiolid-evaluate 0.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 of axiolid-hidden-lines use as well (ADR 0094, #383). The section's output and refusals are unchanged. A state that cannot occur in that code is now GeomError::Degenerate, no longer BackendContractViolation. Wall clock of cargo 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_simple and retriangulate_face refuse a NaN or infinite coordinate with GeomError::InvalidInput. orient2d now reports such a coordinate as uncertain instead of a certain zero (#404), and these entry points would otherwise panic on it. coplanar_overlap keeps its signature and panics on a non-finite coordinate, documented under # Panics.

0.3.15 - 2026-11-03 ​

Changed ​

  • ScalarSection::section_request merges 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 is InPlaneSide::Both. It no longer returns REFUSED_OVERLAPPING_IN_PLANE_FACES or REFUSED_IN_PLANE_FACES_ON_BOTH_SIDES. Every Both region is a cut region when cut regions are requested; a Both ring 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 reports in_plane_crossings, and in_plane_pairs counts 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-exact for the exact points of the cut.

0.3.13 - 2026-10-29 ​

Added ​

  • ScalarSection::section_request traces 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_request sections SectionRole::Sheet meshes: 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 (was MPL-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-evaluate 0.3.17, whose certified frame laws and intrinsic positions it re-exports and axiolid-mesh-compile 0.3.28 uses through it. 0.3.8 accepted any 0.3.12 or later, so a lockfile holding an older axiolid-evaluate did not build axiolid-mesh-compile 0.3.28.

0.3.8 - 2026-10-12 ​

Fixed ​

  • clash::interference counted every narrow-phase pair of well-formed triangles that do not meet (TriangleTriangleRelation::Disjoint) as a degenerate_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 as curve and surface.

0.3.6 - 2026-10-03 ​

Added ​

  • station: axiolid-evaluate's station resolution (#241), re-exported on the same footing as curve and surface.

0.3.5 - 2026-10-03 ​

Added ​

  • Re-exports axiolid_evaluate::arc_parameter and axiolid_evaluate::chain (#239).

0.3.4 - 2026-10-03 ​

Added ​

  • bound, re-exported from axiolid-evaluate like curve and surface (#232).

Fixed ​

  • tessellate_primitive refuses a cylinder or a cone whose chord budget needs more than 4096 segments with BudgetExceeded, 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_primitive keeps 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 with BudgetExceeded instead 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_primitive meshes Primitive::Torus and Primitive::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 LICENSE and LICENSE-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 (was MPL-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 length L in the Botsch–Kobbelt scheme CGAL's isotropic_remeshing follows. Each iteration splits edges longer than 4/3 L, collapses edges shorter than 4/5 L where no edge longer than 4/3 L results, 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 a HalfedgeMesh; 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.
  • RemeshReport measures 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.
  • RemeshError refuses non-manifold, inconsistently wound, ragged or out-of-range input (wrapping HalfedgeBuildError), 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 a HalfedgeMesh by 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, at O(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 over max_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 over max_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 from axiolid-numeric. Affine functions are reproduced, so a planar border keeps the faired vertices in its plane.
  • subdivide and limit_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) and axiolid-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 Preserved and returned a mesh without them.

axiolid-route ​

0.4.0 - 2026-11-13 ​

Changed ​

  • Breaking: requires axiolid-overlay 0.4, whose OverlayError is #[non_exhaustive] with the new HolesOverlap and PolygonsOverlap, and whose Region::new refuses rings that cross into each other. skeleton and CostRegion::new take overlay's Polygon, so a caller builds it with overlay 0.4 (#448, ADR 0104).

0.3.9 - 2026-11-07 ​

Added ​

  • The package now ships LICENSE and LICENSE-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 (was MPL-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_seeded and weighted_distance_map_seeded_within take (point, weight) targets, each starting at its own non-negative cost, as distance_map_weighted does for plain maps (#197); both bounds start there, and WeightedReach::cost counts the weight. With every weight zero the map and its answers are weighted_distance_map's exactly.
  • weighted_forced_walk and weighted_forced_walk_within (#198): the cheapest walk that enters a polygon over two weighted maps with the same costs, bracketed with a witness as forced_walk is. 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::shortest brackets 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_weighted and distance_map_within_weighted take (point, weight) targets, each starting at its own non-negative distance -- a stair landing carrying the rest of a walk beyond it. nearest answers the least route length plus weight, in the new Reach::distance, and names the target the route ends at, which may be a lighter target the heavier one is reached from; route.length is the route's own. farthest_point and forced_walk keep their brackets (the weighted distance is still 1-Lipschitz), and ForcedWalk::shortest counts the origins' weights. With every weight zero the map is distance_map's. MapError::InvalidWeight refuses 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 for farthest_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::shortest is 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_map takes 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 edges spacing apart. WeightedMap::nearest brackets 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_point brackets the farthest point of a subregion by the same branch and bound, each triangle's slope its greatest factor.

  • MapError::InvalidFactor, InvalidSpacing and CostCrossing: 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_map builds 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 most spacing apart, 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 a clearance interval 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_map and distance_map_within (#186): shortest-path distances to the nearest of several targets, from one multi-source Dijkstra over the visibility graph. DistanceMap::nearest gives, for any point of the region, the nearest target's index and the route there (Reach). Refusals are typed: MapError::NoTargets, MapError::TargetOutside.
  • farthest_point and farthest_point_within (#186): the greatest distance to the nearest target over a polygon subregion, as a LengthInterval that 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_path and DistanceMap share 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 DisconnectedComponents again.

axiolid-spatial ​

0.3.6 - 2026-11-07 ​

Added ​

  • The package now ships LICENSE and LICENSE-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 (was MPL-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_coordinates2 and discrete_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 as BarycentricError::Undefined at a pole (a denominator vanishing on the line through a non-adjacent edge, or discrete harmonic's own collinear fan angle). And mean_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) and tetrahedron_barycentric, exact at corners; and mean_value_coordinates2 for 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 with BarycentricError.

axiolid-surface ​

0.3.6 - 2026-11-07 ​

Added ​

  • The package now ships LICENSE and LICENSE-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 (was MPL-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 serde feature, off by default, deriving Serialize and Deserialize for Surface and 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 ​

  • BSplineSurface is defined in axiolid-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. Requires axiolid-curve 0.3.1.

axiolid-tessellation-contract ​

0.3.4 - 2026-11-07 ​

Added ​

  • The package now ships LICENSE and LICENSE-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 (was MPL-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 LICENSE and LICENSE-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 (was MPL-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. orient3d and insphere decide every step exactly; cospherical points, points in the plane of a hull face (decided by in_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 LICENSE and LICENSE-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 (was MPL-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 serde feature, off by default, deriving Serialize and Deserialize for BRep, 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 LICENSE and LICENSE-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_constraints benchmark, 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, with Ring and RingKind: 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 new Triangulation::outside_points (#353).
  • TriangulationError variants RingOutOfRange, TooFewRingVertices, RepeatedRingVertex, FoldedRing, MisnestedRing and EmptyDomain (#353).

Changed ​

  • refine honours the domain of a triangulate_domain result: 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::constraints lists the pieces (#360).

0.3.5 - 2026-10-28 ​

Changed ​

  • The licence is now MPL-2.0 OR LicenseRef-Commercial (was MPL-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 ​

  • MergedPoint and Triangulation::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 ​

  • triangulate merges 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 and Triangulation::constraints use the kept indices, points still holds every input point, and merged_points reports 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.

  • triangulate covers 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 has 2n - 2 - h triangles for n points with h on 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 ​

  • triangulate keeps 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's triangulate_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).
  • triangulate no 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 CrossingConstraints only when a full pass flips nothing.

Released under the Mozilla Public License 2.0.