FixedMathSharp v7.x is a deterministic arithmetic, transform, and full-domain
geometry hardening release. The largest source-breaking change is the
FixedTransform redesign: local and world values are now explicit, parent
changes have named preservation semantics, and arbitrary matrix import is
strict. Arithmetic and geometry APIs also retain exact wide intermediates until
their final Q32.32 conversion, which can change raw results at rounding,
overflow, normalization, degeneracy, and containment boundaries.
Use this guide when upgrading from any v6.x package.
- Update package references to
FixedMathSharpv7.x, orFixedMathSharp.Leanv7.x if you use the Lean package. - Rebuild first and migrate removed
FixedTransform, matrix-scale, andVector3d.ClosestPointsOnTwoLinesusages. Replace any direct use of the removedFixedMath.AddOverflowHelper; it was an unused low-level helper, not part of the fixed-point arithmetic contract. - Decide explicitly whether each transform access is local or world space.
- Audit chained multiply/divide calculations that rely on an intermediate
saturated result; use
TryMultiplyDividewhen the mathematical expression requires one final rounding step. - Replace
ray.Position + ray.Direction * parameterreconstruction withray.GetPoint(parameter)so each coordinate rounds and saturates once. - Replace normalize-then-ray finite-axis sweeps with the segment
physical-distance interval APIs when the authored chord contains small
components that must not round away during normalization. Reconstruct those
hits with
segment.GetPointAtDistance(distance, totalDistance). - Replace separate radial and axial finite-cylinder expansion with
TryGetSweptSphereFiniteCylinderIntersectionDistancewhen the intended volume is the exact swept-sphere Minkowski sum rather than a sharp-rim affine proxy. - Replace
FixedBoundCircle.RadiusSquaredandFixedBoundSphere.RadiusSquaredcallers with the actualRadiusor the bound's exact containment/intersection methods. - Handle
OverflowExceptionfrom bounding-sphere factories when their deterministic containing radius is not representable; v6 could silently return a saturated sphere that did not contain its input. - Re-record deterministic golden values, replay hashes, and serialized expected outputs that depend on division, normalization, transforms, segments, or triangles. Do not silently compare v6 and v7 simulation hashes as though the numeric contract were unchanged.
- Re-run deterministic replay, save/load, collision/query, transform-hierarchy, and broad-phase tests after the source migration compiles.
Bounds and geometry primitives now share the FixedMathSharp.Geometry
namespace. Type names are unchanged; update the namespace import:
using FixedMathSharp.Geometry;Fixed64 multiplication and division now share the same nearest-even rounding
contract. Division previously rounded exact midpoints away from zero. In v7, the
retained raw value's parity decides the tie:
Fixed64 oneRaw = Fixed64.FromRaw(1);
Fixed64 threeRaw = Fixed64.FromRaw(3);
// v7 raw results: 0 and 2.
Fixed64 first = oneRaw / Fixed64.Two;
Fixed64 second = threeRaw / Fixed64.Two;FixedMath.FastDiv matches / for its supported positive-divisor path.
Divide-by-zero and final saturation behavior are unchanged.
This correction also restores exact binary reciprocal identities:
value / Fixed64.Two == value * Fixed64.Half
value / new Fixed64(4) == value * Fixed64.Quarter
value / new Fixed64(8) == value * Fixed64.EighthPre-rounded reciprocals are still different mathematical inputs. For example,
value / new Fixed64(3) is not required to equal
value * Fixed64.FromFraction(1, 3).
The normal Fixed64, Vector2d, and Vector3d addition/subtraction operators
remain saturating. Use the new TryAdd and TrySubtract methods when
saturation must be reported instead of accepted:
if (!Vector3d.TrySubtract(end, start, out Vector3d delta))
{
// The exact component result was outside the Fixed64 range.
}Failure returns false with result = default. Vector operations are atomic;
they never expose a partially calculated or partially saturated vector.
Parentheses do not fuse overloaded operators. (left * right) / divisor still
rounds and saturates the multiplication before division. Use TryMultiplyDivide
when the complete expression should round once and fail only when the divisor is
zero or the final result is not representable:
if (!Fixed64.TryMultiplyDivide(left, right, divisor, out Fixed64 result))
{
// Zero divisor or final-result overflow.
}
if (!Fixed64.TryMultiplyDivide(first, second, third, divisor, out result))
{
// Zero divisor or final-result overflow.
}These are opt-in APIs. Existing operators retain their public saturating contract.
FixedMath.AddOverflowHelper has been removed. For checked integral code, use
the language's checked context. A caller that deliberately needs the former
wrapped-sum-plus-flag behavior can keep that policy locally:
long sum = unchecked(left + right);
overflow |= ((left ^ sum) & (right ^ sum)) < 0;Fixed64.MultiplyAdd(left, right, addend) provides the corresponding fused
multiply-add contract. It rounds once and saturates only the final result;
TryMultiplyAdd reports an unrepresentable final value with false and a
default result. FixedRay.GetPoint(parameter) and
FixedRay2d.GetPoint(parameter) use this contract per coordinate. The argument
is a ray parameter, not always a physical distance, because ray directions are
not normalized by construction.
FixedSegment2d and FixedSegment now expose
TryGetCircleIntersectionDistanceInterval,
TryGetSphereIntersectionDistanceInterval, and
TryGetCapsuleIntersectionDistanceInterval; FixedSegment also exposes
TryGetFiniteCylinderIntersectionDistanceInterval. These methods preserve the
original endpoint differences through the exact radial or finite-axis solve,
then map the closed segment parameter range to the caller-supplied nonnegative
totalDistance with one final round-half-to-even conversion. This is stronger
than normalizing the chord and calling a bounded-ray method: a small but
representable transverse component cannot disappear before collision
classification.
Centered capsule and centered finite-cylinder overloads retain the existing inclusive-start and strict-end containment flags. Endpoint-authored capsule and cylinder overloads provide the same classification. Centered cylinder overloads retain separate authored full axis length, radial expansion, and axial expansion.
For swept spheres, FixedSegment now also exposes
TryGetSweptSphereFiniteCylinderIntersectionDistance and
TryGetSweptSphereFiniteCylinderIntersectionDistanceInterval. These methods
solve the rounded side, cap, and circular-rim boundary of the exact
finite-cylinder/sphere Minkowski sum. They are not aliases for independently
expanding cylinder radius and height, which produces a larger sharp-rim volume.
The entry-only form avoids refining the toroidal-rim exit root when a query
needs only its first contact; both forms retain exact wide intermediates through
one final round-half-to-even physical-distance conversion.
FixedSegment.TryGetSweptSphereBoxIntersectionDistance provides the equivalent
entry-only contract for a FixedBoundBox. Use it when the intended volume is
the exact box/sphere Minkowski sum: independently expanding the three box
extents creates sharp edge and corner regions that can report early or false
contacts. The method preserves the authored chord through exact box-feature
transitions and narrows only the final physical distance.
Use GetPointAtDistance(distance, totalDistance) to reconstruct a returned hit
from the same exact chord. It rejects negative total distance and distances
outside [0, totalDistance]; zero total distance is accepted only for a
zero-length segment and reconstructs its single point. Nondegenerate endpoints
are returned bit-for-bit, while interior coordinates are fused and rounded once.
v7 adds full-domain helpers for calculations that must make decisions before public saturation:
Vector2d.CompareProjection(...)Vector3d.CompareProjection(...)Vector3d.ProjectNonNegativeDifference(...)Vector2d.TryGetMagnitude(...)Vector3d.TryGetMagnitude(...)Vector4d.TryGetMagnitude(...)Vector2d.IsNormalized()FixedMath.Average(first, second, third)FixedMath.Midpoint(left, right)
Vector and quaternion magnitude/normalization paths now preserve complete finite component ranges. Quaternion construction and conversion are also more robust:
- Nonzero vector and quaternion normalization results now satisfy the matching
IsNormalized()predicate. Inputs already admitted by that predicate are preserved, while tiny and full-domain components normalize through a scale-safe exact fallback when ordinary Q32.32 magnitude arithmetic cannot retain enough information. - Nonzero axis and direction inputs normalize scale-safely.
- Zero-axis axis-angle construction returns
FixedQuaternion.Identity. - Radian and degree constructors accept the complete finite
Fixed64domain; multi-turn inputs reduce deterministically. FixedQuaternion.QuaternionLogtolerates one-raw-unit normalization drift at bothAcosendpoints.FixedQuaternion.ToMatrix3x3is independent of a nonzero quaternion's common scale; zero still maps to identity.FixedQuaternion.Anglenow returns the full shortest physical rotation in degrees and treatsqand-qas the same rotation. v6 returned the quaternion half-angle; remove downstream* 2corrections.FixedMath.DegToRadandRadToDegretain full-domain intermediates and round once at the public boundary.FixedMath.SinandCosuse complementary reduced-range polynomials around quadrant boundaries. Values adjacent to0and±Pi/2no longer jump away from their exact anchors by the former degree-7 endpoint error.FixedMath.CanonicalSinCosErrorBoundpublishes the conservative approximation bound for canonical[-Pi, Pi]inputs. It intentionally excludes phase error accumulated while reducing large multi-turn angles.
Ordinary inputs generally retain their expected meaning, but raw results can change where v6 saturated, underflowed, rounded an intermediate, or evaluated trigonometry in the outer half of a quadrant. Refresh golden numeric and replay expectations rather than adding downstream clamps.
FixedPointAnchor and FixedPointAnchor2d can now retain relative geometry
after an offset or absolute point leaves the Q32.32 scalar domain. Prefer their
semantic operations over materializing an intermediate vector:
CompareSquaredDistance(first, second)ranks two candidates exactly from the reference anchor and preserves exact ties.TryGetPointandTryGetOffsetFromremain the honest narrowing boundaries when a public point or vector is actually required.TryGetLocalPointInandTryReframepreserve rigid-frame cancellation without first materializing an absolute world point.
The public anchor contract deliberately excludes rigid-body lever, mass-property, and response policy. Simulation libraries should own those semantics while reusing FixedMathSharp's general geometry and arithmetic.
Vector2d.GetDirection(start, end) and Vector3d.GetDirection(start, end) now
normalize endpoint differences without first narrowing them to one Fixed64
component. Equal endpoints return zero. Use these helpers when world-coordinate
endpoints can span more than one representable scalar even though the resulting
unit direction is representable. Very small representable differences also keep
their component ratio; for example, raw deltas (1, 2) no longer quantize the
magnitude first and return a non-unit direction.
Vector2d.TryGetDistance(start, end, out distance) and
Vector3d.TryGetDistance(start, end, out distance) provide the corresponding
full-domain endpoint-distance contract. They round the final Euclidean distance
to the nearest representable Fixed64; when that positive distance cannot be
represented, they return false and set distance to Fixed64.MaxValue.
Vector2d.Lerp and its in-place form now match the existing full-domain scalar
and 3D interpolation contract: each component retains the complete endpoint
difference until the final nearest-even result. Raw values can therefore change
where the v6 implementation saturated end - start or rounded two weighted
products independently.
Circle/sphere point, pair, area, and box predicates now compare exact wide
squared distances against exact radius sums. Vector2d.CheckDistance and
Vector3d.CheckDistance use the same full-domain contract. Inclusive, strict,
zero-radius, and negative-threshold behavior is unchanged, but classifications
can change where v6 saturated a distance, radius sum, square, or derived bound.
FixedBoundCircle.RadiusSquared and FixedBoundSphere.RadiusSquared were
removed. A Fixed64 cannot represent the square of every valid radius, so those
properties could not honor their advertised domain. Keep the actual radius and
use the exact bound predicates when making spatial decisions.
FixedRay.Intersects(FixedBoundSphere) and
FixedRay2d.Intersects(FixedBoundCircle) now evaluate offset differences,
quadratic products, discriminants, and first-root ordering without saturation.
New first-hit overloads bound the accepted ray parameter and can expand the
target radius in wide arithmetic:
Fixed64? hit = ray.Intersects(targetSphere, sourceRadius, maxParameter);The expansion must be nonnegative. Its exact sum with the target radius may
exceed Fixed64.MaxValue; this is useful for Minkowski-expanded sweeps without
pre-saturating the geometry decision.
Callers that need both radial roots can use TryGetIntersectionInterval on the
same ray types. These methods return the closed overlap interval clipped to an
explicit non-negative maximum parameter and offer overloads that retain radius
expansion exactly instead of pre-adding two saturating Fixed64 radii.
Bounded capsule and finite-cylinder traversal now has the same direct ray-space
contract. Use TryGetCapsuleIntersectionInterval on FixedRay2d or FixedRay,
and TryGetFiniteCylinderIntersectionInterval on FixedRay. These methods clip
directly to [0, maxParameter]; they do not normalize a long traversal into a
segment parameter and multiply the rounded result back into distance. A
normalized direction therefore preserves physical-distance raw units even on a
very long ray. Endpoint-axis, centered-axis, radial-expansion, and 3D affine
cylinder forms mirror the segment families. Advanced overloads report inclusive
origin containment and strict containment at the maximum parameter.
Use FixedMath.TryGetCircleCrossSectionRadius when reducing a sphere at a
signed plane offset. It retains the difference of squares and square root in
wide arithmetic through the final nearest-even radius, returning false only
when the plane does not intersect the sphere. Negative source radii throw.
Use FixedMath.TryGetSphereSlabCrossSectionRadius when projecting a sphere
through a centered finite slab. It keeps opposite-domain center separation and
the slab offset exact before reducing to the nearest cross-section radius.
FixedBoundSphere.CreateFromBoundingBox, CreateFromFrustum,
CreateFromPoints, and CreateMerged now keep construction arithmetic wide
until the final Q32.32 center and radius. Extreme-pair ordering no longer
collapses when multiple squared distances saturate, same-sign endpoint midpoints
no longer saturate before halving, and merge radius sums are halved before
representability is decided.
Successful factories return a deterministic sphere that contains every supplied
point or bound. Non-integral required radii round outward rather than to
nearest, which can increase a v7 radius by one raw unit relative to a rounded
Euclidean distance. If the chosen deterministic construction needs a radius
outside the Fixed64 range, the factory throws OverflowException; it never
returns a saturated under-bound result.
CreateFromPoints and CreateFromFrustum remain deterministic Ritter-style
builders, and a merged center must be selected from the Q32.32 coordinate
lattice. These methods therefore promise containment, not a mathematically
minimum enclosing sphere. Refresh golden values that previously depended on
intermediate saturation or nearest-radius rounding.
FixedBoundArea and FixedBoundBox now derive centers and extents without
saturating endpoint addition or subtraction. Center uses a nearest-even Q32.32
midpoint. Size / Proportions return the exact endpoint span and throw
OverflowException when a positive component cannot fit in Fixed64. Scope
rounds an odd raw-unit span outward so it never under-represents the stored
endpoints; only a half-extent outside the positive scalar domain throws.
This also changes centered construction and mutation at raw-unit boundaries:
- odd raw-unit sizes expand by one raw unit when divided into symmetric half-extents;
- recentering an odd-span bound can expand that axis by one raw unit so the requested lattice center remains exact;
Fixed64.MinValueis valid as a size component because its magnitude can be halved before representability is decided, but it is not valid as a scope component because its positive magnitude is unrepresentable; and- centered factories, center/size setters,
Resize,Orient, andSetBoundingBoxvalidate all endpoints before committing. They throwOverflowExceptionwithout partially mutating an existing bound when an endpoint would leave the scalar domain.
Code that intentionally models only the representable portion of geometry
extending beyond the Q32.32 coordinate domain must opt into
FromCenterAndSizeClippedToDomain or FromCenterAndScopeClippedToDomain. The
explicit name distinguishes domain clipping from an exact centered bound; the
ordinary factories no longer clip silently. FixedBoundCircle.Bounds
deliberately uses the clipped contract so a circle crossing a scalar face still
returns the AABB of its representable domain intersection.
Audit callers that assumed every stored min/max interval had a representable
full-size vector, or that expected odd raw-unit sizes to round inward.
FixedRange.MidPoint now uses the same full-domain midpoint contract, and
FixedRange.Length throws rather than saturating an unrepresentable signed
endpoint difference. Spatial indexes should use
FixedBoundBox.GetVolumeExpansionCost instead of multiplying derived
Proportions: it retains both union volumes in exact unsigned 192-bit
arithmetic and clamps only the final integer heuristic to long.MaxValue.
FixedOrientedBox is the new invariant-bearing 3D oriented-box primitive. It
stores only a center, normalized orientation, and strictly positive local
half-extents. Construction rejects non-normalized quaternions and non-positive
half-extents; default(FixedOrientedBox) is deliberately invalid.
The public feature API stays center-relative. GetLocalCorner and
GetLocalSupportPoint select stable local features, while
TryMaterializeLocalPoint performs the one final exact local-to-world
conversion only when a world-space witness is actually needed.
GetBoundsClippedToDomain computes analytical extents without materializing or
deforming saturated world corners.
Contains, TryGetClosestPointOnSurface, and GetNearestFaceNormal use one
exact scale-invariant rational basis derived from the stored quaternion's raw
components. GetAxes exposes nearest-even Fixed64 views of those conceptual
axes; the rounded views are not reused for classification. Support ties retain
the lower corner index, and nearest-face ties select X, then Y, then Z. Equality
remains structural: q and -q produce identical geometry but remain distinct
stored orientations.
Materialization rounds a conceptual local-to-world result once to its nearest even Q32.32 lattice point. Because a conceptual face or corner can lie between lattice points, a rounded boundary witness is not guaranteed to classify as contained. Use an inset local point when the materialized result must remain strictly inside.
Both the standard and Lean packages expose the same oriented-box API.
FixedOrientedBox supports constructor-validated JSON serialization but is
intentionally not MemoryPack-annotated: MemoryPack serializes unmanaged structs
as raw memory and therefore cannot enforce the constructor invariant. It also
does not advertise field-based binary serialization for the same reason. Persist
authored center/orientation/half-extents in a validated DTO when a binary
payload is required.
FixedTransform no longer hides one mutable matrix behind ambiguous component
properties. Its authoritative state is now local position, normalized local
rotation, and exact signed or zero local scale. World values are derived from
the parent chain.
Migrate properties by intent:
| v6.x surface | v7.x local replacement | v7.x world replacement |
|---|---|---|
Position |
LocalPosition |
WorldPosition or TrySetWorldPosition(...) |
Rotation |
LocalRotation |
WorldRotation or TrySetWorldPose(...) |
Scale |
LocalScale |
LossyScale |
LossyScale alias |
LocalScale |
LossyScale with hierarchy-derived semantics |
EulerAngles |
LocalEulerAngles |
No writable world-Euler alias |
Writable Parent |
SetParentKeepingLocal(...) |
TrySetParentKeepingWorld(...) |
new FixedTransform(matrix) |
Component constructor | TryCreateFromLocalMatrix(...) for strict import |
For example:
// v6.x: names and parent-assignment behavior were ambiguous.
transform.Position = localPosition;
transform.Parent = parent;
// v7.x: local mutation and reparenting intent are explicit.
transform.LocalPosition = localPosition;
transform.SetParentKeepingLocal(parent);
// Use this instead when the current world pose must be preserved.
bool reparented = transform.TrySetParentKeepingWorld(parent);Parent is read-only. Reparenting rejects self/ancestor cycles.
TrySetParentKeepingWorld, TrySetWorldPosition, and TrySetWorldPose commit
atomically only when the required inverse and strict TRS decomposition succeed.
The v6 matrix constructor accepted arbitrary matrices. In v7, import is explicit and strict:
if (!FixedTransform.TryCreateFromLocalMatrix(
localMatrix,
out FixedTransform? transform,
parent))
{
// Perspective, shear, singular/zero scale, unrepresentable, or
// non-round-trippable matrix.
}Prefer the component constructors at engine-adapter boundaries. They preserve
signed and zero authored local scale without asking FixedMathSharp to infer
components from a matrix. Calls using named constructor arguments must also
rename position, rotation, and scale to localPosition, localRotation,
and localScale.
LocalToWorldMatrix, WorldPosition, and LossyScale traverse parents on
read. FixedTransform does not own a scene graph, child collection, matrix
cache, or engine object.
Generic local/world point conversion now lives on FixedTransform.
TransformPoint and InverseTransformPoint use the complete composed affine
hierarchy; TryTransformPoint and TryInverseTransformPoint return false for
unrepresentable or singular cases. The explicit *PointXZ variants are the
planar bridge and reject hierarchy coupling with Y.
The new LocalPositionXZ, LocalRotationXZRadians, LocalScaleXZ,
WorldPositionXZ, and WorldRotationXZRadians helpers embed planar (x, y) as
3D (x, 0, y). Local position and scale setters preserve the existing Y
component. Positive planar rotation matches Vector2d.Rotate; local and world
angle getters report the projected local-right direction modulo Fixed64.TwoPi.
Use LossyScale.ToVector2d() only when a canonical hierarchy-derived X/Z scale
view fits the consuming system.
Matrix scale APIs now distinguish unsigned basis magnitudes from a canonical signed lossy view:
| v6.x surface | v7.x replacement |
|---|---|
Fixed3x3.ExtractScale(...) |
Fixed3x3.ExtractScaleMagnitudes(...) |
matrix3x3.ExtractScale() |
matrix3x3.ExtractScaleMagnitudes() |
Fixed4x4.ExtractScale(...) |
Fixed4x4.ExtractScaleMagnitudes(...) |
matrix4x4.ExtractScale() |
matrix4x4.ExtractScaleMagnitudes() |
Fixed4x4.Scale |
Fixed4x4.LossyScale |
Fixed3x3.SetLossyScale(scale) |
Fixed3x3.CreateScale(scale) |
Fixed3x3.SetScale(...) |
Fixed3x3.CreateScale(...) for pure scale |
Fixed3x3.ResetScaleToIdentity(...) |
Own rotation and scale components explicitly |
Fixed3x3.SetGlobalScale(...) |
Own rotation and scale components explicitly |
Fixed4x4.SetScale(...) |
Checked Decompose + CreateTransform |
Fixed4x4.ResetScaleToIdentity(...) |
Checked Decompose + CreateTransform |
Fixed4x4.SetGlobalScale(...) |
Checked Decompose + CreateTransform |
ExtractScaleMagnitudes always returns nonnegative basis magnitudes.
ExtractLossyScale and Fixed4x4.LossyScale now derive basis magnitudes and
assign an odd reflection's canonical negative sign to X. They no longer return
the matrix diagonal. If a caller genuinely needs diagonal entries, read M11,
M22, and M33 explicitly and do not label them scale.
Callers that cannot accept a saturated basis magnitude should use
Fixed4x4.TryExtractLossyScale. It preserves the same canonical reflection
contract, accepts singular, sheared, and non-affine bases, and returns false
with zero only when a final row magnitude is not representable.
FixedTransform.TryGetLossyScale combines this with strict hierarchy
composition. Fixed4x4.TryTransformAffinePoint similarly rejects non-affine
matrices and computes each row-vector point coordinate with one exact
round-half-to-even conversion, returning zero atomically when any coordinate is
not representable.
For rigid-frame geometry that also applies a local scale, use
FixedQuaternion.TryTransformScaledPoint in 3D or
Vector2d.TryTransformScaledPoint in 2D. Their overloads retain
origin + rotation * (localPoint * scale) as one wide operation and return
false only when a final coordinate is outside the scalar domain; they avoid
the intermediate saturation of separately scaling, rotating, and translating.
The matching 3D inverse, FixedQuaternion.TryInverseTransformScaledPoint,
retains world subtraction, inverse rotation, and component division through the
same one-final-rounding contract. It returns false atomically for a zero
quaternion, singular scale, or unrepresentable final local coordinate.
SetScale and ResetScaleToIdentity only overwrote diagonal entries and
corrupted rotated bases. Construct a pure scale matrix with CreateScale.
SetGlobalScale was also removed: a matrix value has no parent or global
context, and these mutation APIs had no failure channel for reflections, shear,
or singular rows. For 3x3 transforms, keep authored rotation and scale as
explicit components instead. Fixed3x3.GetNormalized is not a general
reflected-scale remover.
Fixed4x4.Decompose(...) keeps its Boolean signature but now returns false
for matrices that are not valid affine orthogonal TRS values. Check the result:
if (!Fixed4x4.Decompose(matrix, out Vector3d translation,
out FixedQuaternion rotation, out _))
{
throw new InvalidOperationException("Matrix is not a valid affine TRS value.");
}
Fixed4x4 rebuilt = Fixed4x4.CreateTransform(
translation,
rotation,
desiredScale); // Use Vector3d.One to remove scale.The existing 2D/3D segment and triangle query surfaces now preserve exact raw endpoint differences, predicates, ratios, and distance ordering until the final public conversion. This fixes extreme-coordinate and near-degenerate cases but can change raw results relative to v6.
v7 also adds FixedTriangle.TryGetContact(...) for consumers replacing local
rigid triangle SAT implementations. Supply each triangle's origin and normalized
rotation; the result provides a first-to-second normal, one final half-even
depth with explicit clamp state, and one anchor in each rigid frame. Exact
touching is included, while exact-zero normals and negative interval overlap
return false.
New segment APIs replace downstream line/segment solvers:
var first2d = new FixedSegment2d(a2d, b2d);
var second2d = new FixedSegment2d(c2d, d2d);
bool unique = first2d.TryGetUniqueIntersection(
second2d,
out Fixed64 firstParameter,
out Fixed64 secondParameter);
(Vector2d firstPoint, Vector2d secondPoint) =
first2d.GetClosestPoints(second2d);Vector3d.ClosestPointsOnTwoLines(...) was removed because it actually solved
finite segments. Replace it with the accurately owned API:
// v7.x
var first = new FixedSegment(firstStart, firstEnd);
var second = new FixedSegment(secondStart, secondEnd);
(Vector3d firstPoint, Vector3d secondPoint) = first.GetClosestPoints(second);Finite-axis intersections should also move to the segment-owned interval APIs instead of normalizing an axis or building quadratic coefficients downstream:
bool capsuleHit = query.TryGetCapsuleIntersectionInterval(
capsuleAxis,
capsuleRadius,
out Fixed64 capsuleEntry,
out Fixed64 capsuleExit);
bool centeredCapsuleHit = query.TryGetCapsuleIntersectionInterval(
capsuleCenter,
normalizedCapsuleAxis,
capsuleAxisLength,
capsuleRadius,
radiusExpansion,
out Fixed64 centeredCapsuleEntry,
out Fixed64 centeredCapsuleExit);
bool cylinderHit = query.TryGetFiniteCylinderIntersectionInterval(
cylinderAxis,
cylinderRadius,
out Fixed64 cylinderEntry,
out Fixed64 cylinderExit);
bool centeredCylinderHit = query.TryGetFiniteCylinderIntersectionInterval(
cylinderCenter,
normalizedCylinderAxis,
cylinderAxisLength,
cylinderRadius,
radialExpansion,
axialExpansion,
out Fixed64 centeredEntry,
out Fixed64 centeredExit);Expanded overloads accept authored radius and expansion separately. The centered
finite-cylinder overload accepts the positive full authored axis length and a
separate axial expansion, avoiding a narrowed combined radius or expanded cap
center. Capsule zero lengths reduce to a circle or sphere; cylinder zero axes
throw because the segment cannot retain a cap normal. Returned closed [0, 1]
parameters are rounded half to even, so deterministic query goldens produced by
an older downstream quadratic should be regenerated.
When the original query is a ray plus a finite travel budget, keep it in ray space instead of constructing a segment solely to reuse these methods:
bool rayCapsuleHit = ray.TryGetCapsuleIntersectionInterval(
capsuleCenter,
normalizedCapsuleAxis,
capsuleAxisLength,
capsuleRadius,
radiusExpansion,
maxParameter,
out Fixed64 entryDistance,
out Fixed64 exitDistance,
out bool originContained,
out bool maximumContainedStrict);With a normalized ray direction the two returned parameters are physical
distances. With any other direction they remain ordinary ray parameters. Use the
centered capsule or cylinder overload when a physical center and normalized axis
are the source of truth, especially near Fixed64.MinValue or
Fixed64.MaxValue. Neither contract constructs cap centers, so scalar
saturation cannot silently shorten or rotate the finite axis. A centered capsule
accepts zero length as the circle/sphere limit; a centered cylinder requires a
positive full length. Invalid zero or non-unit axis directions are rejected.
FixedBoundArea.FromCenteredCapsuleClippedToDomain and
FixedBoundBox.FromCenteredCapsuleClippedToDomain derive tight analytical 2D
and 3D axis-aligned bounds from that full-length capsule contract. Exact
conceptual endpoints are rounded outward and clipped only at the final scalar
domain boundary.
Centered capsule containment, closest-feature direction, and surface
reconstruction are available through ContainsPointInCenteredCapsule,
GetDirectionFromCenteredAxis, and TryGetSurfacePointOnCenteredCapsule. The
surface helper accepts an explicit normalized radial direction so callers can
choose the otherwise non-unique on-axis result. It fuses the conceptual axis
point and radial offset before one final half-to-even conversion, returning
false only when the final surface coordinate cannot be represented.
Callers that previously cached cap centers or reconstructed convex support
points should keep center, normalized axis, full length, and radius as the
canonical state. Use TryGetCenteredAxisEndpoint for an explicitly selected cap
and the centered capsule, finite-cylinder, or finite-cone Try*Support helper
for a final support witness. These helpers preserve odd raw lengths and return
false atomically when the selected world point is not representable.
Centered capsule containment has explicit strict overloads in 2D and 3D;
strict mode excludes the cylindrical side and both rounded-cap boundaries while
retaining the optional radial-expansion contract. Centered finite cylinders
provide the matching ContainsPointInCenteredFiniteCylinder(..., strict)
helper, where strict mode also excludes both flat caps.
GetDistanceToCenteredCapsule and TryGetDistanceToCenteredCapsule provide the
matching exact distance contract without reconstructing a surface point. They
return zero for points inside or on the capsule and round a positive gap half to
even only at the final Fixed64 conversion. The Get form saturates an
unrepresentable final distance to Fixed64.MaxValue; the Try form returns
false and also writes Fixed64.MaxValue.
FixedSegment, FixedSegment2d, FixedTriangle, and FixedTriangle2d now use
one final nearest-even conversion and final-only saturation for their hardened
query results. Triangle barycentric weights are calculated independently;
degenerate closest-point ties retain stable AB, BC, CA order. No source change
is normally required, but collision, containment, and deterministic golden tests
should be rerun.
FixedMathSharp.Chronicler.WriteTransform now hashes authoritative local
position, local rotation, and local scale in that order. Parent identity and all
derived world views are excluded.
This is an intentional replay/hash compatibility boundary. Version stored replays or regenerate v7 golden hashes rather than expecting hashes produced from the v6 matrix-backed transform representation to match.
After updating package references, these searches catch the main v7 migration work:
rg -n "new FixedTransform\s*\(" src tests
rg -n "\.(Position|Rotation|Scale|EulerAngles|PositionXZ|RotationXZRadians|ScaleXZ)\b" src tests
rg -n "\.Parent\s*=" src tests
rg -n "ExtractScale|SetLossyScale|\.Scale\b" src tests
rg -n "ClosestPointsOnTwoLines" src tests
rg -n "AddOverflowHelper" src tests
rg -n "WriteTransform" src testsReview property matches by intent instead of mechanically adding Local.
Physics and simulation state often wants local components, while rendering,
queries, and hierarchy-aware bounds may want derived world values.
FixedMathSharp v6.x is a geometry and bounds hardening release. The largest
change is dimensional clarity: FixedBoundBox is the 3D AABB type, while
FixedBoundArea is now a true Vector2d 2D AABB. The release also removes
ambiguous bounds construction, removes hidden corner-array storage, adds shared
2D/3D segment and triangle primitives, and adds an optional
FixedMathSharp.Chronicler companion package for deterministic replay hash
writers.
Use this guide when upgrading from any v5.x package.
- Update package references to
FixedMathSharpv6.x, orFixedMathSharp.Leanv6.x if you use the lean package. - Add
FixedMathSharp.ChroniclerorFixedMathSharp.Chronicler.Leanonly if your project uses Chronicler replay hashing helpers. - Replace old 3D
FixedBoundAreausage. UseFixedBoundBoxfor 3D volumes and the newVector2d-basedFixedBoundAreafor planar footprints. - Replace
new FixedBoundBox(center, size)with named factory calls. - Replace
FixedBoundBox.VerticeswithGetCornerorCopyCorners. - Audit
Intersectsbehavior where touching bounds used to be treated as separate. Default intersections are now boundary-inclusive. - Audit serialized or hashed bounds payloads if you persisted
FixedBoundAreaorFixedBoundSpherestate directly. - Re-run deterministic replay, save/load, spatial-query, and broad-phase tests.
FixedBoundArea no longer represents a 3D shape. It is now a normalized 2D
axis-aligned area backed by Vector2d:
FixedBoundArea area = FixedBoundArea.FromMinMax(
new Vector2d(-4, -2),
new Vector2d(4, 2));Migrate old 3D area usage by intent:
| v5.x usage | v6.x replacement |
|---|---|
| 3D volume, collider, frustum, ray, or plane bounds | FixedBoundBox |
| Flat footprint in a 3D world | FixedBoundArea plus explicit layer, height, or elevation state in the consuming package |
| Pure 2D area query or broad-phase bounds | FixedBoundArea |
There is no 3D FixedBoundArea compatibility layer. That is intentional: a 3D
area was ambiguous beside FixedBoundBox, and higher-level packages should own
layer/elevation semantics explicitly.
3D overloads that previously accepted FixedBoundArea were removed. For
example:
// v5.x 3D-shaped area usage
// bool hit = ray.Intersects(area) != null;
// FixedPlaneIntersectionType side = plane.Intersects(area);
// v6.x 3D volume usage
FixedBoundBox box = FixedBoundBox.FromMinMax(min3d, max3d);
Fixed64? hit = ray.Intersects(box);
FixedPlaneIntersectionType side = plane.Intersects(box);
// v6.x pure 2D usage
FixedRay2d ray2d = new(origin2d, direction2d);
Fixed64? planarHit = ray2d.Intersects(area);The public FixedBoundBox(Vector3d center, Vector3d size) constructor was
removed because call sites could not tell whether two vectors meant center/size,
center/scope, or min/max. Use named factories:
// v5.x
FixedBoundBox box = new FixedBoundBox(center, size);
// v6.x
FixedBoundBox box = FixedBoundBox.FromCenterAndSize(center, size);
FixedBoundBox fromHalfExtents = FixedBoundBox.FromCenterAndScope(center, scope);
FixedBoundBox fromCorners = FixedBoundBox.FromMinMax(min, max);FromMinMax, SetMinMax, and serialized state population normalize swapped
min/max inputs. FromCenterAndSize and FromCenterAndScope normalize negative
extents by absolute component value.
The serialization state constructor remains:
FixedBoundBox box = new FixedBoundBox(
new FixedBoundBox.BoundingBoxState(min, max));Use that constructor for explicit state transfer, not for ordinary call-site construction.
FixedBoundBox.Vertices was removed. It exposed mutable array storage from a
value type and could allocate when callers only needed a corner.
// v5.x
Vector3d corner = box.Vertices[0];
// v6.x
Vector3d corner = box.GetCorner(0);
Span<Vector3d> corners = stackalloc Vector3d[FixedBoundBox.CornerCount];
box.CopyCorners(corners);GetCorner and CopyCorners use the same stable corner order. Prefer
GetCorner for one-off access and caller-owned spans/arrays for bulk copies.
Default Intersects methods now use closed-bound, boundary-inclusive overlap.
Touching edges, faces, corners, or tangent surfaces count as intersections.
Where positive area or positive volume matters, use IntersectsStrict:
bool touchesOrOverlaps = box.Intersects(otherBox);
bool hasPositiveVolume = box.IntersectsStrict(otherBox);
bool areasTouchOrOverlap = area.Intersects(otherArea);
bool hasPositiveArea = area.IntersectsStrict(otherArea);Strict overlap methods reject boundary-only contact and zero-size inputs. They exist for box-box, box-sphere, sphere-box, sphere-sphere, area-area, area-circle, circle-area, and circle-circle pairs.
If your v5.x logic depended on FixedBoundBox.Intersects(FixedBoundBox)
returning false for face, edge, or corner contact, switch that call site to
IntersectsStrict.
FixedBoundCircle is new in v6.x. FixedBoundSphere was also tightened so
negative radii are normalized through construction, assignment, and serialized
state load.
FixedBoundSphere sphere = new FixedBoundSphere(center, new Fixed64(-5));
// sphere.Radius == 5FixedBoundSphere now exposes a matching BoundingSphereState payload, aligned
with FixedBoundBox, FixedBoundArea, and FixedBoundCircle.
If your project persisted or inspected bounds state directly, audit those payloads:
- 3D boxes write canonical
MinthenMax. - 2D areas write canonical
Vector2d MinthenVector2d Max. - 2D circles write
Centerthen normalizedRadius. - 3D spheres write
Centerthen normalizedRadius.
v6.x adds reusable deterministic geometry primitives that downstream packages can use instead of local one-off structs:
| Domain | New type | Main use |
|---|---|---|
| 2D area bounds | FixedBoundArea |
Planar AABB, clamp/project, union, area overlap |
| 2D circular bounds | FixedBoundCircle |
Radius queries, circle/area overlap, projection |
| 2D rays | FixedRay2d |
Planar ray against area/circle |
| 2D segments | FixedSegment2d |
Finite edge, closest point, distance, bounds |
| 2D triangles | FixedTriangle2d |
Area, bounds, containment, closest point, barycentric weights |
| 3D segments | FixedSegment |
Finite 3D edge, closest point, distance, bounds |
| 3D triangles | FixedTriangle |
Normal, area, bounds, containment, closest point, projected barycentric weights |
Segments preserve ordered endpoint identity. Reversed endpoints have the same bounds but do not compare equal.
Rays do not normalize direction by construction. A returned ray parameter is a physical distance only when the caller supplied a normalized direction.
Triangles preserve ordered vertices. FixedTriangle2d.TryGetBarycentricWeights
solves planar weights directly.
FixedTriangle.TryGetProjectedBarycentricWeights names the 3D projection
behavior explicitly.
Vector2d.BarycentricCoordinates(...) now mirrors
Vector3d.BarycentricCoordinates(...) for reconstructing points from known B/C
barycentric weights.
FixedMathSharp v6.x includes an optional FixedMathSharp.Chronicler companion
package. Add it only when your project uses Chronicler replay hashing:
using Chronicler;
using FixedMathSharp.Chronicler;
ChronicleHashWriter writer = new();
writer.WriteFixed64(value);
writer.WriteVector3d(position);
writer.WriteBoundBox(bounds);The extension package writes canonical deterministic payloads for Fixed64,
vectors, quaternions, transforms, matrices, bounds, rays, and planes. It keeps
Chronicler-specific code out of the core math package.
Important FixedBoundArea change: WriteBoundArea now writes the new 2D
FixedBoundArea. If old replay code used it for 3D area-like payloads, migrate
that hash input to WriteBoundBox or to an explicit 2D area plus separate
layer/elevation fields.
After updating package references, these searches catch the most common v6 migration work:
rg -n "new FixedBoundBox" src tests
rg -n "Vertices" src tests
rg -n "FixedBoundArea" src tests
rg -n "Intersects" src tests
rg -n "WriteBoundArea" src testsReview each new FixedBoundBox(...) match manually. The state constructor is
still valid, while the old center/size constructor should become a named factory
call.
Review each FixedBoundArea match by dimension. If the surrounding code uses
Vector3d, a ray/plane/frustum, or volumetric bounds, it probably wants
FixedBoundBox. If the code is planar, migrate to the new Vector2d area.
FixedMathSharp v5.0.0 is a major API hardening release. The migration is mostly source-level cleanup, but several changes affect numeric interpretation, transform semantics, and public names.
Use this guide when upgrading from v4.0.1 or earlier.
- Update package references to
FixedMathSharp5.0.0, orFixedMathSharp.Lean5.0.0 if you use the lean package. - Rebuild your solution and fix compile errors before chasing runtime behavior. Many v5 changes intentionally fail at compile time instead of preserving weak v4 shapes.
- Replace renamed geometry and enum types.
- Audit every raw
Fixed64text conversion. - Replace removed floating-point helper surfaces with explicit
Fixed64boundary conversions. - Audit matrix, quaternion, and transform code that assumed column-vector or engine-specific semantics.
- Re-run deterministic replay, save/load, and lockstep tests after the code compiles.
The bounds types now use the same Fixed* naming style as the rest of the
library.
| v4.x | v5.0.0 |
|---|---|
BoundingBox |
FixedBoundBox |
BoundingSphere |
FixedBoundSphere |
BoundingArea |
FixedBoundArea |
BoundingFrustum |
FixedBoundFrustum |
ContainmentType |
FixedEnclosureType |
The core geometry namespace remains FixedMathSharp. Most call sites need a
rename only:
// v4.x
BoundingBox room = new BoundingBox(center, size);
ContainmentType state = room.Contains(other);
// v5.0.0
FixedBoundBox room = new FixedBoundBox(center, size);
FixedEnclosureType state = room.Contains(other);Fixed64 now makes value-space and raw-payload conversions explicit.
In v4.x, Fixed64.Parse and TryParse interpreted text as a raw Q32.32 long
payload. In v5.0.0, they parse normal decimal value text.
// v4.x raw payload text
Fixed64 one = Fixed64.Parse("4294967296");
// v5.0.0 raw payload text
Fixed64 one = Fixed64.ParseRaw("4294967296");
// v5.0.0 value-space decimal text
Fixed64 value = Fixed64.Parse("1.25");Use TryParseRaw for raw payload text and ToRawString when writing raw text.
Use Parse, TryParse, ToString, and TryFormat for human-readable decimal
diagnostics.
The public double constructor was removed. Floating-point input now goes through checked boundary factories.
// v4.x
Fixed64 speed = new Fixed64(3.5);
Fixed64 ratio = Fixed64.Fraction(1, 60);
// v5.0.0
Fixed64 speed = Fixed64.FromDouble(3.5);
Fixed64 exactDecimal = Fixed64.FromDecimal(3.5m);
Fixed64 ratio = Fixed64.FromFraction(1, 60);Vector2d, Vector3d, and Vector4d double constructors were also replaced
with FromDouble factories:
// v4.x
Vector3d point = new Vector3d(1.25, 2.5, 3.75);
// v5.0.0
Vector3d point = Vector3d.FromDouble(1.25, 2.5, 3.75);FromDouble, explicit float/double casts to Fixed64, vector FromDouble
factories, and FixedCurveKey.FromDouble now reject NaN and infinities with
ArgumentOutOfRangeException. Finite values outside the Q32.32 range throw
OverflowException.
The Fixed64 arithmetic operators that accepted long operands were removed.
Those overloads were ambiguous because a long can mean either a normal integer
value or an already-scaled raw Q32.32 payload.
long tileCount = 5;
long rawStep = 1;
Fixed64 integerValue = (Fixed64)tileCount;
Fixed64 rawValue = Fixed64.FromRaw(rawStep);The explicit long conversion now saturates to Fixed64.MinValue or
Fixed64.MaxValue when the source integer is outside the representable Q32.32
whole-number range.
Several cross-domain helpers were removed so deterministic code stays in fixed-point land.
| v4.x | v5.0.0 |
|---|---|
FixedRange.InRange(double) |
Convert once with Fixed64.FromDouble, then call InRange(Fixed64) |
DeterministicRandom.NextDouble() |
Use NextFixed6401() or NextFixed64(...) |
Fixed64.RawToString() |
Fixed64.ToRawString() |
Fixed64.RawToInt(...) |
Fixed64.ToInt(...) |
Keep floating-point conversion at engine, UI, editor, or import/export
boundaries. Core simulation code should pass Fixed64 values directly.
FixedMath is the canonical home for scalar algorithms such as interpolation,
powers, logarithms, trigonometry, square root, rounding, and clamping. Fixed64
owns representation, constants, conversions, parsing, operators, equality, and
raw helpers.
For v4 call sites, the verified scalar rename is:
| v4.x | v5.0.0 |
|---|---|
value.ToDegree() |
value.ToDegrees() |
For new or refactored scalar interpolation code, prefer the FixedMath static
surface, such as FixedMath.Lerp, FixedMath.CatmullRom,
FixedMath.HermiteSpline, and FixedMath.BarycentricCoordinate. The fluent
extension surface is curated and forwards to the canonical implementation.
Factories and convention-heavy methods stay on the owning type.
Vector mutation and value-returning APIs now use one naming model.
| v4.x | v5.0.0 |
|---|---|
vector.x, vector.y, vector.z, vector.w |
vector.X, vector.Y, vector.Z, vector.W |
vector.Normal |
vector.Normalized |
vector.Normalize() |
vector.NormalizeInPlace() |
Vector*d.Normalize(value) where applicable |
Vector*d.GetNormalized(value) |
SqrMagnitude |
MagnitudeSquared |
SqrDistance(...) |
DistanceSquared(...) |
Vector2d.Lerped(...) |
Vector2d.Lerp(...) |
ScaleInPlace(...) |
MultiplyInPlace(...) |
Public vector-result out Vector*d helpers |
Return-by-value statics or explicit *InPlace methods |
Example:
// v4.x
Vector3d normal = velocity.Normal;
velocity.Normalize();
velocity.ScaleInPlace(factor);
// v5.0.0
Vector3d normal = velocity.Normalized;
velocity.NormalizeInPlace();
velocity.MultiplyInPlace(factor);If you persist vectors through JSON using field names, audit payloads that use
lowercase component names. MemoryPack component order remains explicit through
the existing [MemoryPackOrder] attributes.
Normalize(out Fixed64 magnitude) remains as
NormalizeInPlace(out Fixed64 magnitude) because it returns a second scalar
result.
v5.0.0 makes affine transform semantics explicit and consistent:
- FixedMathSharp's 3D basis is
+Xright,+Yup, and+Zforward. - 3D transforms use row vectors: points and vectors are transformed as
value * matrix. - Translation lives in
M41,M42, andM43. Fixed4x4composition is left-to-right under that row-vector convention.- Quaternion matrix conversion, matrix-to-quaternion extraction, Euler extraction, and look-rotation basis construction were aligned to that model.
Audit any code that depended on a column-vector convention, copied matrices
directly from engine APIs, or assumed a -Z forward convention.
Fixed4x4.Decompose now returns out parameters in
translation, rotation, scale order.
// v4.x style
Fixed4x4.Decompose(matrix, out Vector3d scale, out FixedQuaternion rotation, out Vector3d translation);
// v5.0.0
Fixed4x4.Decompose(matrix, out Vector3d translation, out FixedQuaternion rotation, out Vector3d scale);Use CoordinateConvention3d at adapter boundaries instead of changing core
direction constants or adding engine-specific conditionals.
CoordinateConvention3d external = CoordinateConvention3d.NegativeZForward;
Vector3d canonicalForward = external.ToCanonicalDirection(external.Forward);Built-in conventions include:
CoordinateConvention3d.CanonicalCoordinateConvention3d.PositiveZForwardCoordinateConvention3d.NegativeZForwardCoordinateConvention3d.XForwardZUp
These helpers map direction vectors and signed axes. Matrix storage, handedness, clip-space depth, units, and origins remain adapter-specific concerns.
FixedBoundSphere.CreateFromPoints now has Vector3d[] and
ReadOnlySpan<Vector3d> overloads for countable, allocation-light call sites.
The IEnumerable<Vector3d> overload remains for interoperability.
Vector2d.CheckDistance and Vector3d.CheckDistance now compare squared
distances after validating the threshold, avoiding an unnecessary square root.
Negative thresholds throw ArgumentOutOfRangeException.
FixedMath.FastAdd, FastSub, FastMul, FastDiv, and FastMod are expert
APIs. Their docs describe the skipped checks or precision caveats. Prefer the
normal operators unless a local benchmark proves that the fast path is correct
for your inputs and worth the narrower contract.
Human-readable formatting is now separated from raw payload representation:
- Use
ToString(...)orTryFormat(...)for logs, editor display, and diagnostics. - Use
ToRawString,ParseRaw, andTryParseRawfor raw Q32.32 payload text. - Use MemoryPack or JSON support for structured serialization.
On net8.0, supported types implement ISpanFormattable. On netstandard2.1,
the same TryFormat method shape is exposed where the interface itself is
unavailable.