Problem
The kernel had no in-plane coordinate frame with behaviour. Frame2 and Frame3 in axiolid-core are inert storage: they hold axes but own no operations and enforce no invariant. Every consumer therefore re-derived the same two mappings and its own validity rule.
Four separate orthonormality checks existed under three different tolerance policies:
| site |
policy |
planar/project/src/lib.rs |
tolerance.linear() |
sampled/field/src/config.rs |
tolerance.angular() |
parametric/evaluate/src/surface.rs |
hardcoded 1e-9 |
execution/dispatch/src/section.rs |
ad hoc |
The defect this caused
Orthonormality is decided by a dot product of two unit vectors, which is dimensionless. project::Plane::validate compared it against the linear tolerance, tying a pure direction test to the model length unit.
Under Tolerance::MILLIMETRE the linear tolerance is 1e-3, so a basis skewed by up to 0.5 milliradians passed as orthonormal. Projection then used a non-orthogonal basis as if it were orthogonal, and every projected coordinate was silently wrong. The same basis was correctly refused under Tolerance::METRE.
Failing test before the fix:
a_skewed_plane_is_refused_under_millimetre_tolerance ... FAILED
a 0.5 mrad skewed basis must be refused regardless of the length unit
Fix
Added PlaneFrame to axiolid-core: private fields, validated construction, and both directions of the map (project, lift, signed_distance, normal).
- validity is decided with the angular tolerance, so it no longer depends on the length unit
- fields are private, so a skewed basis is unrepresentable rather than merely rejected
- the normal is derived (
x cross y) rather than stored, so it cannot disagree with the axes
from_normal builds a valid frame when the caller cares about the plane but not the in-plane x direction, seeding against the least-aligned world axis for stability
axiolid_project::Plane is now an alias for it. The old ProjectionError::InvalidPlane is unreachable and documented as such, retained because removing a public variant is breaking.
Verification
- 6 new tests, including a round-trip invariant and a
from_normal-always-valid property over 6 normals
- mutation-tested: reverting
angular() to linear() makes a_skewed_basis_is_refused_in_every_unit_system fail, so the guard is real rather than decorative
- workspace 1107 passed / 0 failed
Problem
The kernel had no in-plane coordinate frame with behaviour.
Frame2andFrame3inaxiolid-coreare inert storage: they hold axes but own no operations and enforce no invariant. Every consumer therefore re-derived the same two mappings and its own validity rule.Four separate orthonormality checks existed under three different tolerance policies:
planar/project/src/lib.rstolerance.linear()sampled/field/src/config.rstolerance.angular()parametric/evaluate/src/surface.rs1e-9execution/dispatch/src/section.rsThe defect this caused
Orthonormality is decided by a dot product of two unit vectors, which is dimensionless.
project::Plane::validatecompared it against the linear tolerance, tying a pure direction test to the model length unit.Under
Tolerance::MILLIMETREthe linear tolerance is1e-3, so a basis skewed by up to 0.5 milliradians passed as orthonormal. Projection then used a non-orthogonal basis as if it were orthogonal, and every projected coordinate was silently wrong. The same basis was correctly refused underTolerance::METRE.Failing test before the fix:
Fix
Added
PlaneFrametoaxiolid-core: private fields, validated construction, and both directions of the map (project,lift,signed_distance,normal).x cross y) rather than stored, so it cannot disagree with the axesfrom_normalbuilds a valid frame when the caller cares about the plane but not the in-plane x direction, seeding against the least-aligned world axis for stabilityaxiolid_project::Planeis now an alias for it. The oldProjectionError::InvalidPlaneis unreachable and documented as such, retained because removing a public variant is breaking.Verification
from_normal-always-valid property over 6 normalsangular()tolinear()makesa_skewed_basis_is_refused_in_every_unit_systemfail, so the guard is real rather than decorative