ADR 0024: Curves and surfaces domains, statuses and numeric guarantees¶
Status: Accepted for G0. Later phases implement these contracts; changing one
requires amending this record. Amended in G3: sweep mesh generation has its own
public header, sweep.hpp (see the boundary table).
Context¶
The curves and surfaces architecture proposes prepared paths, tracks and patches above the implemented G1 cubic and bicubic kernel in FoundationMath. Its G0 gate requires frozen domains, status and tolerance vocabularies, a representative fixture set, agreement on which results are guarantees and which are estimates, and an audit of header, dependency and format boundaries before G2 creates owners. The architecture keeps the rationale; this record fixes the names, rules and classes that G2-G5 code and tests must use.
Decision¶
Domains¶
| Value | Meaning | In memory | Range and unit | Encoded field |
|---|---|---|---|---|
SegmentParameter |
Span index and local curve parameter | usize SpanIndex, float64 U |
U in [0,1], dimensionless | uint32, float64 |
SurfaceParameter |
Patch index and local patch parameters | usize PatchIndex, float64 U, float64 V |
[0,1]^2, dimensionless | uint32, 2 x float64 |
PathDistance |
Prefix arc length from the path start | float64 |
Declared asset units, [0, total length] | float64 |
TrackTime |
Seconds from the track's local epoch | float64 |
Seconds, [first key, last key] | float64 |
These are distinct types with no implicit conversion. Controls are float32
relative to a float64 chunk origin; sources may store float64 coordinates.
Distance is measured in the asset's local metric (see the architecture for
transform rules). A surface parameter is never a texture coordinate.
Span selection. An exact interior key or span boundary selects the outgoing
span at u = 0. The terminal endpoint of an open domain selects the last span at
u = 1. SpanSide::Incoming must be requested explicitly and selects the
previous span at u = 1; at the first endpoint there is no incoming span, so
both sides select the first span. A Step span holds its left key's value for u
in [0,1) and the right key's value only at u = 1.
Derivative units. Kernel derivatives are per normalized u. For a span of
domain width h, the domain derivative is (dP/du)/h and the second derivative
(d2P/du2)/(h*h). Track derivatives are value units per second. Surface
partials are per normalized U and V.
Boundary policies. They apply only outside the closed domain [start, end] of duration D = end - start; inside it every policy evaluates the same value.
| Policy | Valid for | Outside the domain |
|---|---|---|
Reject |
All | InvalidDomain; outputs unchanged |
Clamp |
All | Evaluate the nearest endpoint; the derivative is the one-sided derivative from inside the domain |
Hold |
Tracks | Nearest endpoint value with zero derivatives |
Loop |
Tracks | Evaluate start + mod(x - start, D), which lies in [start, end) |
PingPong |
Tracks | Reflect with period 2D; the reflected half negates the first derivative |
Wrap |
Closed paths | As Loop, over parameter or distance; an open path rejects it as InvalidArgument |
No policy extrapolates. A Loop of a nonperiodic track jumps at its seam; that
is authored behavior, not a runtime repair.
Status vocabulary¶
FoundationMath keeps MathStatus. Preparation, sampling and queries above it
return a GeometryStatus with exactly these enumerators:
| Enumerator | Meaning |
|---|---|
Success |
Outputs are written and meaningful |
NonFiniteInput |
A NaN or infinity where a finite value is required |
InvalidArgument |
A malformed request or profile: negative tolerance, unknown policy, Wrap on an open path |
InvalidDomain |
A parameter, time or distance outside its domain under Reject; nonincreasing keys; coincident consecutive waypoints |
InvalidTopology |
Unsupported or inconsistent adjacency, edge or corner records |
MissingCache |
The snapshot lacks the cache the request needs; sampling never builds it |
StaleRevision |
A cursor, view or cache revision does not match its snapshot |
Degenerate |
A requested direction or normal is undefined at that point; position may still be valid |
DegenerateFrame |
A framed profile cannot define a transported frame run under its policy |
IllConditioned |
Below a named conditioning threshold |
OutOfRange |
Finite inputs whose result cannot be represented |
UnsupportedRepresentation |
Valid source geometry this version cannot represent, such as rational or trimmed input |
UnsupportedPrecision |
The requested tolerance is below the arithmetic floor, or a strict profile needs a certificate that is unavailable |
BudgetExceeded |
A work, depth, iteration or output budget was exhausted |
CapacityExceeded |
A caller-owned output range is too small; detected before writing |
OutOfMemory |
A fallible allocation failed |
Cancelled |
The caller cancelled preparation |
A geometric miss is still Success with a result classification, as in
FoundationMath. MathStatus crosses the boundary unchanged by name, except
that an invalid kernel parameter becomes InvalidDomain and SizeMismatch
becomes InvalidArgument. Enumerator names are frozen; numeric values and
multi-failure precedence are not an ABI or file format, so serialized
diagnostics store names. Diagnostics carry the code, resource and source
element, property path, requested tolerance, achieved bound with its guarantee
class, and the offending range with its domain kind. Bad content is a status,
never an assertion.
Guarantee classes¶
| Class | Meaning | Satisfies a strict profile |
|---|---|---|
| Exact | Bitwise defined: stored endpoints and corners, decisions on stored values, exact identities | Yes |
| Rounded | One computation with float64 intermediates narrowed to float32; no error bound is promised | Not as a bound |
| Certified | An enclosure from reviewed outward-rounded arithmetic, validated with the pinned native and Wasm toolchains | Yes |
| Estimated | A heuristic, padded or sampled bound; reported with its class | No |
| Approximate | A separately named mode that returns its candidate and achieved residual | No |
A strict preparation profile accepts only Exact or Certified bounds; otherwise
it fails with UnsupportedPrecision and leaves the active snapshot unchanged.
Visual profiles may accept Estimated bounds and must report them. No outward
rounding helper exists yet, so strict profiles are unavailable until G2 adds
and validates one.
| Result | Phase | Class |
|---|---|---|
| Kernel endpoint and corner positions | G1 | Exact |
| Kernel interior positions, derivatives, split controls, Hermite controls and normals | G1 | Rounded |
TryPatchNormal Degenerate/IllConditioned decision |
G1 | Exact for exactly zero partials; Rounded near a threshold |
| Hull of a span's stored controls, enclosing its exact curve | G2 | Exact |
| Hulls of subdivided leaves and transformed bounds | G2 | Certified with outward rounding, otherwise Estimated |
| Chord/control-polygon length interval and prefix uncertainty | G2 | Certified with outward accumulation, otherwise Estimated |
| Midpoint length estimate | G2 | Estimated, inside the interval |
| Distance inversion residual bound | G2 | Certified from Certified partial-span enclosures |
| Linear distance-table lookup | G2 | Approximate |
| Strict projection global distance gap | G3 | Certified |
ProjectPointApproximate |
G3 | Approximate |
| Transported frame angular error | G3 | Estimated until derivative enclosures are validated |
| Sweep self-intersection risk | G3 | Estimated; never a proof of simplicity |
| Control-net plus twist position error of a tessellation cell | G4 | Certified with outward rounding |
| Tessellation normal-angle error | G4 | Estimated unless derivative enclosures are Certified |
| Continuity classification | G2, G4 | Threshold decision on Rounded values, reported with its thresholds |
| Float32 position floor | G0 | Exact policy value |
| Performance and memory budgets | All | Unmeasured |
Tolerance profile¶
| Field | Unit | Valid values | Use |
|---|---|---|---|
PositionError |
Asset units | Finite, > 0, at least the position floor | Absolute geometric error |
RelativePositionError |
Dimensionless | [0, 1) | Effective error is max(PositionError, relative x declared asset scale) |
DistanceError |
Asset units | Finite, > 0 | Whole-path prefix gap, not a per-leaf tolerance |
TangentAngleError |
Radians | (0, pi/2) | Frame and tangent accuracy |
ProjectionGap |
Asset units | Finite, > 0 | Global upper/lower distance gap |
ContinuityPosition |
Asset units | Finite, >= 0 | C0 classification |
ContinuityDerivative |
Dimensionless | [0, 1) | Relative difference of domain-scaled derivatives for C1/C2 |
ContinuityAngle |
Radians | [0, pi/2) | G1 direction classification |
NormalMinDerivativeLength |
Asset units per unit parameter | Finite, >= 0 | Same meaning as PatchNormalPolicy |
NormalMinSinAngle |
Dimensionless | [0, 1) | Same meaning as PatchNormalPolicy |
NormalAngleError |
Radians | (0, pi/2) | Tessellation normal accuracy |
CoincidenceDistance |
Asset units | Finite, > 0 | Source waypoint coincidence; editors merge visibly |
Work limits (sizes, leaves, depth, iterations, scratch and output bytes) are a
separate record with no engine defaults. Validation returns NonFiniteInput
or InvalidArgument; a request below the floor returns UnsupportedPrecision.
Position floor. For chunk-relative controls whose largest absolute
component is E > 0 (in the float32 normal range), the floor is
2 * ulp32(E), with ulp32(E) = 2^(floor(log2 E) - 23). Storing a control
rounds each component by at most half a unit; the convex hull keeps outputs
within E, so rounding a result adds at most another half. One unit per
component gives at most sqrt(3) units in length, below two. This is a
representability floor, not a promise that any computation achieves it. A
chunk whose extent cannot meet a requested error must be split or rejected.
Representative fixture set¶
tests/fixtures/curves_surfaces/generate_fixtures.py generates
reference_fixtures.hpp from exact rational and 50-digit decimal arithmetic
without Ludus code; test_fixtures.py checks its oracles against closed forms
and exact identities and rejects a stale header. The header is test-only, is
not installed, and is reached through the ludus_geometry_reference_fixtures
target. Regenerate with python3 tests/fixtures/curves_surfaces/generate_fixtures.py --write
then ./scripts/check --format --fix.
| Group | Fixtures | Main consumers |
|---|---|---|
| Cubic spans | line_uniform, line_ease, line_backtrack, cusp, parabola, inflection, twisted, vertical_start, arch |
G1 samples; G2 lengths, zero speed and inversion; G3 frames and projection ties |
| Patches | plane, saddle, bulge, apex, edge_first, edge_second |
G1 samples and normals; G4 tessellation bounds and singular normals |
| Shared edge | reversed_shared_edge |
G4 orientation mapping and corner identity |
| Waypoints | centripetal_uneven, duplicate_waypoint |
G2 nonuniform Catmull-Rom conversion and rejection |
| Tracks | mixed_track, duplicate_time |
G2 Step/Linear/Hermite, span sides, every boundary policy, overshoot |
| Precision | far_origin |
G0/G2 origin subtraction and the position floor |
Each fixture's Purpose string names what it exercises. Adding fixtures is
routine. Changing an existing reference changes a frozen contract and requires
amending this record. The evidence ledger
records the corpus checks and the G1 cross-check.
Header, dependency and format boundaries¶
| Target | Public headers | May depend on |
|---|---|---|
Ludus::FoundationMath (implemented) |
cubic.hpp, patch.hpp |
Base only |
Ludus::FoundationGeometry at modules/foundation/geometry (G2) |
status.hpp, path.hpp, track.hpp, distance.hpp, frame.hpp, surface.hpp, query.hpp, sweep.hpp, tessellation.hpp |
Base, Math, Containers |
Ludus::GeometryFormat at modules/content/geometry_format (G5) |
Versioned cooked record layouts, limits and constants | Base only (header-only INTERFACE target) |
Ludus::GeometryContent at modules/geometry/content (G5) |
Source codecs, validation, cooking, loading, publication | Content, FoundationGeometry, GeometryFormat |
G3 amendment: sweep planning and emission live in sweep.hpp, not in
tessellation.hpp. A framed path consumer then parses no mesh API, and the
patch tessellation header that G4 adds stays independent of path sweeps.
status.hpp includes only Base. No Geometry header is added to core.h or the
foundational PCH. No public Geometry header includes Content, RHI, Logging,
Threading or a heavy standard header; consumers log and profile. The cooked
format lives in its own target, so a runtime loader never links a cooker and a
cooker never links the renderer. That follows the repository rule for formats
shared by tools and runtime and refines the architecture's earlier placement of
the format inside GeometryContent.
Cooked fields are fixed-width little-endian values written field by field:
uint32 indices and counts, float32 controls, float64 origins, domains and
tolerances. Never encode usize, pointers, native layouts, SIMD registers or
FoundationMath values. Readers validate count products, offsets, alignment,
overlap, enum values, monotone domains and finiteness before exposing a view.
Consequences¶
G2 creates FoundationGeometry with these names and the fixture corpus as its
first regression input. Strict profiles stay unavailable until a Certified
helper exists. G1 behavior is unchanged; the audit documented its public
declarations and renamed the split outputs to outLeft and outRight, which
is source compatible.
Not decided here: default work limits, performance and memory budgets, GPU profiles, rational, trimmed or subdivision representations, editor commands and the cooked schema's concrete records. Each needs its own evidence in the phase that introduces it.