Ludus::FoundationGeometry¶
Prepared spatial paths, scalar tracks, bounded distance maps, rotation-minimizing frames, closest-point projection, sweep meshes, prepared patch surfaces and crack-free surface meshes above the FoundationMath cubic and bicubic kernel. These are phases G2, G3 and G4 of the curves and surfaces architecture. Names, statuses, domains and guarantee classes follow ADR 0024. Validation is recorded in the evidence ledger for G2, G3 and G4.
The module depends on Ludus::FoundationBase, Ludus::FoundationMath and
Ludus::FoundationContainers. It is not in core.h or the foundational PCH.
Include the header you use:
| Header | Contents |
|---|---|
status.hpp |
GeometryStatus, GuaranteeClass, domain values, SpanSide, BoundaryPolicy, ToleranceProfile, WorkLimits, ComputePositionFloor, GeometryDiagnostic; includes only Base |
path.hpp |
PathSource, TryPreparePath, PreparedPath, PathView, sampling, tangents, control bounds, joint continuity, Hermite and centripetal Catmull-Rom conversion |
distance.hpp |
TryBuildDistanceMap, PreparedDistanceMap, TryBoundDistance, TryResolveDistance |
track.hpp |
TrackKey, TryPrepareTrack, PreparedTrack, TryEvaluateTrack |
frame.hpp |
FramePreparation, TryBuildFrames, PreparedFrames, TryEvaluateFrame, seed, singularity, corner and closed-loop policies |
query.hpp |
ProjectionScratch, TryProjectPoint, ProjectPointApproximate |
sweep.hpp |
SweepRequest, TryPlanSweepMesh, SweepMeshPlan, TryEmitSweepMesh |
surface.hpp |
SurfaceSource, TryPrepareSurface, PreparedSurface, SurfaceView, patch/edge/corner records, surface evaluation and normals, TryMapEdgeParameter |
tessellation.hpp |
SurfaceMeshProfile, TryBuildSurfaceMesh, SurfaceMesh, SurfaceMeshView, SurfaceMeshReport |
Contracts¶
Every function is noexcept; fallible ones return [[nodiscard]] GeometryStatus
and leave outputs unchanged on failure. Preparation builds a complete candidate
and moves it into the output owner only on success, so a failed rebuild keeps the
previous snapshot. Preparation reports the first failure through an optional
GeometryDiagnostic. Sampling, inversion and conversion into caller-owned ranges
never allocate. Owners are move-only and single-owner; views borrow them and are
invalidated by moving or destroying the owner.
- Domains.
SegmentParameteris a span index and u in [0,1].PathDistanceis prefix arc length in asset units.TrackTimeis seconds. They are separate types. Kernel derivatives are per normalized u; track derivatives are per second. - Span sides. An exact boundary selects the outgoing span at u = 0. The
terminal endpoint selects the last span at u = 1.
SpanSide::Incomingselects the previous span at u = 1; at the first endpoint both sides select the first span. A Step span holds its left value until u = 1. - Boundary policies. Policies act only outside the closed domain. Paths
accept
Reject,Clampand, when closed,Wrap. Tracks acceptReject,Clamp,Hold,LoopandPingPong. Every other combination isInvalidArgument, and nothing extrapolates. - Precision. Source controls are float64. Preparation subtracts the chunk
origin in float64, then narrows to float32. The effective position error,
max(
PositionError,RelativePositionErrorxAssetScale), must be at least the float32 position floor2 * ulp32(E)of the narrowed controls and their narrowing error. Otherwise preparation returnsUnsupportedPrecision; split the asset into chunks or choose a nearer origin. - Connectivity. Consecutive spans, and the closing joint of a closed path,
must meet within
ContinuityPosition(InvalidTopologyotherwise). Each joint's C0/G1/C1/C2 classification compares derivatives scaled by each span'sDomainWidthand stores the measured differences. It is a threshold decision on Rounded values. - No defaults.
ToleranceProfileandWorkLimitshave no engine defaults. Every tolerance field is validated against its ADR 0024 range, and a zero work limit allows no work.
Guarantees¶
| Result | Class |
|---|---|
| Stored endpoints, key values, span control boxes, position floor | Exact |
| Interior positions, derivatives, tangents, conversions, continuity values | Rounded |
| Leaf and total length bounds, prefix bounds, inversion residual | Certified |
TotalEstimate, PrefixEstimate, DistanceBound::Estimate |
Estimated, inside the Certified interval |
Frame regularity decision (derivative box versus MinimumSpeed) |
Certified |
| Frame vectors and positions | Rounded |
FramesView::AngularErrorEstimate, residual twist |
Estimated |
Strict projection Distance, LowerBound and Gap |
Certified |
ProjectPointApproximate result |
Approximate |
| Sweep ring frames and vertices | Rounded |
| Sweep longitudinal and cross-section error, self-intersection risk | Estimated |
| Shared-edge matching, corner identity, mesh corner positions | Exact |
| Surface positions, partials, normals, edge continuity values | Rounded |
Mesh cell bounds (SurfaceMeshPatch::PositionBound), emitted position error bounds |
Certified |
| Mesh normal-angle estimate | Estimated |
Certified bounds come from FoundationMath's outward-rounded enclosures.
A strict DistancePreparation (the default) accepts them; there is no Estimated
fallback mode yet. Linear table lookup (an Approximate mode) is not provided.
Paths¶
#include <ludus/foundation/geometry/distance.hpp>
#include <ludus/foundation/geometry/path.hpp>
using namespace ludus::foundation::geometry;
using ludus::foundation::BorrowedView;
using ludus::foundation::math::Vector3d;
// Waypoints with uneven spacing: centripetal Catmull-Rom uses sqrt(chord) knots.
const Vector3d points[] = {{0, 0, 0}, {1, 0, 0}, {1, 4, 0}, {10, 4, 0}};
PathSpanSource spans[3];
usize spanCount = 0;
GeometryStatus status = TryConvertCentripetalCatmullRom(
BorrowedView<const Vector3d>(points), false, 1e-6, BorrowedView<PathSpanSource>(spans), spanCount);
PreparedPath path;
if (IsSuccess(status))
{
const PathSource source{.Spans = BorrowedView<const PathSpanSource>(spans, spanCount), .Revision = 1};
status = TryPreparePath(source, PathPreparation{.Tolerance = tolerance, .Limits = limits}, path, nullptr);
}
TryConvertCentripetalCatmullRom uses the nonuniform Barry-Goldman tangent at
interior waypoints and the one-sided chord tangent at open endpoints. It rejects
consecutive waypoints within coincidenceDistance as InvalidDomain; editors
should offer a visible merge. A closed source wraps neighbors. It must not repeat
its first waypoint at the end: the repeated pair is coincident, so it is rejected.
TryConvertHermite applies B1 = P0 + h*T0/3 and B2 = P1 - h*T1/3 with each
waypoint's explicit OutgoingWidth. Both conversions check output capacity
before writing (CapacityExceeded).
TryEvaluatePath returns a chunk-relative position, or a CubicSample3 with
derivatives per u. Add PathView::Origin in float64 for asset coordinates.
TryEvaluatePathTangent returns Degenerate where |dP/du| is at or below a
caller-supplied speed, for example at a cusp, a zero-speed ease endpoint or a
stationary span. Position at those points remains valid.
Distance maps¶
TryBuildDistanceMap brackets each leaf's arc length between its chord and its
control-polygon length, using outward rounding. It splits the leaf with the
largest gap at its parameter midpoint, with ties broken by earlier span and
parameter, until the whole-path gap TotalUpper - TotalLower is at most the
path's DistanceError. The gap is rechecked in path order before success.
MaxLeaves, MaxDepth, MaxSubdivisions and MaxPreparedBytes bound the work.
Exhausting a limit returns BudgetExceeded, reports the achieved Certified gap in
the diagnostic and keeps the previous map.
PreparedDistanceMap map;
TryBuildDistanceMap(path.GetView(), DistancePreparation{.Limits = limits}, map, nullptr);
DistanceResolution at;
const DistanceQuery query{.Distance = PathDistance{.Value = 2.5}, .Policy = BoundaryPolicy::Clamp,
.MaxIterations = 128};
if (IsSuccess(TryResolveDistance(path.GetView(), map.GetView(), query, at)))
{
// |L(at.Parameter) - 2.5| <= at.ResidualBound <= DistanceError (Certified).
}
The PathDistance domain is [0, TotalEstimate]. TryResolveDistance finds
the leaf by monotone lookup over prefix estimates, then bisects partial-span
estimates for the earliest parameter that reaches the target. It accepts only
when the Certified residual, which includes every preceding leaf's uncertainty,
fits DistanceError. A target equal to a stored boundary estimate resolves to
that exact boundary. Zero-length runs resolve to their earliest parameter, and
the path end resolves to the last span at u = 1. Near zero speed, u(s) is
ill-conditioned even though its residual is bounded; request a tangent to detect
that case. Allow at least 64 iterations so bisection can reach float64
resolution. TryBoundDistance returns the Certified prefix enclosure at any
parameter.
A map records its path's Revision and span count. Queries through an unbuilt
map return MissingCache, and queries through a map of another revision return
StaleRevision. Sampling never builds or repairs a cache. Distance uses the
asset's local metric; nonuniform scale needs a separate world-metric cache.
Tracks¶
#include <ludus/foundation/geometry/track.hpp>
const TrackKey keys[] = {
{.Time = 0.0, .Value = 0.0F, .Interpolation = TrackInterpolation::Linear},
{.Time = 1.0, .Value = 1.0F, .OutSlope = 2.0F, .Interpolation = TrackInterpolation::Hermite},
{.Time = 3.0, .Value = 1.0F, .Interpolation = TrackInterpolation::Step},
{.Time = 3.5, .Value = 0.0F},
};
PreparedTrack track;
TrackSample sample;
if (IsSuccess(TryPrepareTrack(BorrowedView<const TrackKey>(keys), limits, track, nullptr)))
{
TryEvaluateTrack(track.GetView(), TrackTime{.Seconds = 4.5}, BoundaryPolicy::PingPong, SpanSide::Outgoing,
sample);
// Reflected to 2.5 s: Value 1.1875, DerivativePerSecond +0.625 (negated on the reflected half).
}
Key times must be strictly increasing; equal times are InvalidDomain, and the
runtime never resolves a tie. Each key names the interpolation of the span that
starts at it. Hermite spans use the left key's OutSlope and the right key's
InSlope, in value units per second. They may overshoot, and evaluation never
clamps the result. Author a timed hold as equal values over a positive interval.
Hold returns the endpoint value with zero derivative. Clamp returns the
endpoint value with the one-sided derivative from inside the domain.
Transported frames¶
#include <ludus/foundation/geometry/frame.hpp>
FramePreparation preparation{
.SeedUp = Vector3d{0, 1, 0},
.SeedPolicy = FrameSeedPolicy::UseFallbackAxis,
.FallbackAxis = Vector3d{0, 0, 1},
.SingularityPolicy = FrameSingularityPolicy::SplitRuns,
.Limits = limits,
};
PreparedFrames frames;
if (IsSuccess(TryBuildFrames(path.GetView(), map.GetView(), preparation, frames, nullptr)))
{
TransportedFrame frame;
TryEvaluateFrame(path.GetView(), frames.GetView(), SegmentParameter{.SpanIndex = 0, .U = 0.4}, roll, frame);
// frame.Tangent, frame.Normal and frame.Binormal = Tangent x Normal are right-handed and orthonormal.
}
TryBuildFrames bakes rotation-minimizing frames with the double-reflection
method of Wang, Juttler, Zheng and Liu. It needs the path's distance map, which
supplies sample distances and the closed-loop twist distribution. Each span is
stepped over dyadic intervals. An interval is regular when an outward-rounded
box of its derivative stays farther than MinimumSpeed from zero. A regular
interval is accepted when one reflection step and two half steps differ by at
most TangentAngleError times its share of the path. The summed differences,
plus the largest one, form AngularErrorEstimate. That value is Estimated, so a
Strict frame preparation returns UnsupportedPrecision.
The first run starts from SeedUp projected perpendicular to the first tangent.
When that projection is degenerate (its sine is at or below MinimumSinAngle),
RejectDegenerateSeed fails with DegenerateFrame. UseFallbackAxis projects
FallbackAxis instead and records FrameRun::IsFallbackSeeded.
| Case | Policy | Result |
|---|---|---|
| Zero speed, cusp or reversal inside a span | FrameSingularityPolicy::Reject |
DegenerateFrame with the offending parameter range |
SplitRuns |
The run ends before the range; a new run is seeded after it | |
Joint that is not G1 under ContinuityAngle |
FrameCornerPolicy::Reject |
DegenerateFrame |
RotateMinimally |
Rotates about the incoming x outgoing tangent; an exact reversal fails | |
SplitRuns |
A new run is seeded on the outgoing span | |
| Closed path | ClosedFramePolicy::DistributeTwist |
Adds -ResidualTwist + 2 pi WindingTurns of roll in proportion to estimated distance; the seam becomes periodic |
PreserveHolonomy |
Keeps the transported frames and reports ResidualTwist |
A range that cannot be shown regular at the depth limit is 2^-MaxDepth wide.
TryEvaluateFrame returns Degenerate inside it and never reuses a frame
across it. Evaluation transports the preceding baked sample of the same run to
the analytic tangent with one reflection step, so its result depends only on the
cache and the request, not on earlier queries. It then applies the interpolated
twist compensation and the caller's roll. A joint has an incoming sample at
u = 1 and an outgoing sample at u = 0.
Projection¶
#include <ludus/foundation/geometry/query.hpp>
usize capacity = 0;
ComputeProjectionScratchCapacity(path.GetView(), 4096, capacity);
ProjectionScratch scratch;
scratch.TryReserve(capacity); // Once; queries never allocate.
ProjectionResult nearest;
const ProjectionQuery query{.Point = Vector3d{1.5, 0, 0}, .MaxIterations = 4096};
if (IsSuccess(TryProjectPoint(path.GetView(), query, scratch, nearest)))
{
// Exact distance to nearest.Parameter <= nearest.Distance <= global minimum + ProjectionGap (Certified).
}
TryProjectPoint runs a branch-and-bound over control-hull boxes. Each span
starts as one interval; span starts and an open path's terminal endpoint are
candidates. Every iteration splits the interval with the smallest lower bound at
its midpoint and evaluates the midpoint. The search stops when the best
candidate minus the smallest live lower bound fits the path's ProjectionGap.
All bounds use outward rounding, and the query point's origin subtraction is
enclosed too. Any candidate within the gap of the global lower bound is an
acceptable answer; the result is the earliest by span, then parameter. Symmetric
minima therefore return the earlier one, and a minimum at a joint is named by
the outgoing span at u = 0.
Strict projection leaves its result unchanged on BudgetExceeded or
UnsupportedPrecision. ProjectPointApproximate runs the same search but
returns its earliest best candidate when work runs out, with IsGapMet false and
Guarantee Approximate. Scratch must hold span count + 1 + MaxIterations
entries; a smaller reservation is CapacityExceeded before any work.
Sweeps¶
#include <ludus/foundation/geometry/sweep.hpp>
const SweepProfileKey profile[] = {{.Distance = PathDistance{0.0}, .Scale = 0.1}};
SweepRequest request{
.Section = SweepSection::Tube,
.Profile = BorrowedView<const SweepProfileKey>(profile),
.Caps = SweepCaps::Flat,
.LongitudinalError = 1e-3,
.CrossSectionError = 1e-3,
.MaxSectionSegments = 64,
.MaxVertexCount = 65536,
.MaxIndexCount = 393216,
.Limits = limits,
};
SweepMeshPlan plan;
if (IsSuccess(TryPlanSweepMesh(path.GetView(), map.GetView(), frames.GetView(), request, plan, nullptr)))
{
// Size caller storage from plan.GetView().Report, then emit without further failure points.
TryEmitSweepMesh(path.GetView(), plan.GetView(), vertices, indices);
}
Planning chooses rings and validates every count, limit and numeric range
before emission writes anything. It needs frames with one run over the whole
path and G1 joints; split a path at singularities or corners and sweep each
piece. Each span starts a ring and is stepped over dyadic intervals. An interval
is accepted when its control hull's distance from the chord plus the midpoint
deviation of every section point fits LongitudinalError. A tube uses the
fewest polygon segments whose error at the largest profile scale fits
CrossSectionError. Both errors are Estimated. The width or radius (Scale)
and roll are linear in estimated path distance between profile keys and clamped
outside them. A zero or negative scale is rejected, not normalized away.
A ribbon ring emits its -Binormal then +Binormal edge, faces the frame normal and
has no caps. A tube ring repeats its first vertex with U = 1 as an explicit UV
seam. SweepCaps::Flat closes an open tube with fans whose rim vertices copy
the ring positions bitwise but carry the cap normal. V is estimated path
distance in asset units. A closed path needs periodic frames
(DistributeTwist); its last ring copies the first ring's positions and normals
with V equal to the total length. Triangles are counter-clockwise from the side
their normals face unless SweepWinding::Clockwise is requested.
TryEmitSweepMesh checks the pinned path revision (StaleRevision) and both
capacities (CapacityExceeded) before writing. The report's
SelfIntersectionRiskCount counts rings whose section half-extent times path
curvature reaches 1. This sampled local check is Estimated: it can flag a risk
but never proves the surface simple.
Surfaces¶
A SurfaceSource lists untrimmed bicubic patches, each with a unique author
Id and float64 controls Control[u][v], plus explicit
SurfaceAdjacencySource records that join two whole patch sides. Each side has
an along-edge parameter s: v for U0 and U1, u for V0 and V1. A join
declares whether the second side runs opposite to the first (IsReversed).
#include <ludus/foundation/geometry/surface.hpp>
SurfacePatchSource patches[2] = {/* Id = 10 and Id = 20, float64 controls */};
// The first patch's u = 1 side is the second patch's u = 0 side, with v reversed.
const SurfaceAdjacencySource join{.FirstPatch = 0, .FirstSide = PatchSide::U1,
.SecondPatch = 1, .SecondSide = PatchSide::U0, .IsReversed = true};
PreparedSurface surface;
const GeometryStatus status = TryPrepareSurface(
SurfaceSource{.Patches = BorrowedView<const SurfacePatchSource>(patches),
.Adjacency = BorrowedView<const SurfaceAdjacencySource>(&join, 1), .Revision = 1},
SurfacePreparation{.Tolerance = tolerance, .Limits = limits}, surface, nullptr);
TryPrepareSurface narrows controls as TryPreparePath does, then builds the
topology. V1 accepts only matching whole sides with at most two incident patches
per edge. The two narrowed control curves must be equal after the declared
mapping. A mismatched curve, a partial join, a third incidence on a side, a side
joined to itself, an unknown patch index or a duplicate patch ID is
InvalidTopology; nothing is welded by a distance threshold. A side may join
another side of the same patch, which closes it into a tube.
The view holds one SurfaceEdge per edge and one SurfaceCorner per corner.
An edge's owner is the incidence with the smallest (patch ID, side). Its
SurfaceEdgeId and canonical control curve come from that owner, so they do not
depend on patch order. TryMapEdgeParameter converts a canonical edge
parameter into each incident patch's (u, v). Corners joined through shared edges
form one record with one exact position and every incident patch corner.
Corners that only coincide geometrically remain separate records.
Shared edges also report two properties, without guessing smoothness.
IsOrientationConsistent says whether the two patches traverse the edge in
opposite directions along their counterclockwise (u, v) boundaries, so their
Cross(DU, DV) normals agree in orientation. IsC1 compares the outward
cross-boundary derivative controls; it is a threshold decision on Rounded
values. G1 tangent-plane classification is not provided.
TryEvaluateSurface returns positions or all partials per normalized U and V.
TryEvaluateSurfaceNormal uses the surface's NormalMinDerivativeLength and
NormalMinSinAngle. It returns Degenerate for zero or parallel partials and
IllConditioned at or below the thresholds; the position remains valid.
Surface meshes¶
TryBuildSurfaceMesh tessellates every patch as a dyadic grid of
2^LevelU x 2^LevelV cells. Each cell becomes two triangles along its
(u0, v0)-(u1, v1) diagonal, counterclockwise in (u, v). The function builds a
complete SurfaceMesh staging owner and moves it into the output only on
success.
#include <ludus/foundation/geometry/tessellation.hpp>
const SurfaceMeshProfile profile{
.Precision = PrecisionProfile::Strict, .PositionError = 0.01,
.Degeneracy = SurfaceDegeneracyPolicy::Report,
.Limits = {.MaxLevel = 10, .MaxCellTests = 1 << 20, .MaxVertices = 1 << 20,
.MaxTriangles = 1 << 20, .MaxOutputBytes = 64 << 20}};
SurfaceMesh mesh;
if (IsSuccess(TryBuildSurfaceMesh(surface.GetView(), profile, mesh, nullptr)))
{
const SurfaceMeshView view = mesh.GetView();
// view.Report.PositionBound <= 0.01 (Certified); upload view.Vertices and view.Indices.
}
The cell bound adds three terms. The first is the largest distance between the
subpatch controls and the degree-elevated bilinear interpolant of its corners.
The second is the twisted bilinear quad's distance to its two triangles,
Length(P00 - P10 - P01 + P11)/4. The third is the rounding error of the
emitted vertices. Each term uses outward-rounded enclosures, so
PositionBound is Certified for the parametric distance between the prepared
surface and the mesh.
Every patch starts at level (0, 0). While a cell misses the profile, the direction whose refinement gives the clearly smaller normalized bound is refined. Near-ties refine the coarser direction, then u. Each shared edge then takes the larger of its two along-edge levels. Patches whose levels changed are rechecked until the network agrees. This can over-tessellate; for example, a planar patch beside a curved edge inherits that edge's level.
SurfaceMeshView::Positions emits each corner and each edge sample once. Edge
samples are evaluated on the canonical curve, interior samples on their patch.
Every SurfaceMeshVertex copies its shared position bitwise and names its
PositionIndex, while its normal and Parameter belong to its own patch. Two
patches therefore meet without cracks or T-junctions, and their normal or
parameter seams stay visible. A surface parameter is never a texture coordinate.
Failure behavior is explicit:
Strictaccepts only Certified position bounds. A nonzeroNormalAngleErrorisUnsupportedPrecision, because only an Estimated normal bound exists.IsCoarserMeshAcceptedisInvalidArgument.Visualmay requestNormalAngleError. Refinement then also samples analytic normals at cell corners and centres, skipping singular samples, and reports an Estimated angle. WithIsCoarserMeshAccepted, a patch thatMaxLevelstops keeps its finest grid. The mesh then reportsIsWithinPositionError = falseand the achieved Certified bound instead of failing.MaxCellTests,MaxVertices,MaxTrianglesandMaxOutputBytesalways fail withBudgetExceeded. Counts are checked before allocation. More than 2^32 vertices isOutOfRange.SurfaceDegeneracyPolicy::Rejectfails on a singular vertex normal or a triangle with two equal positions.Reportemits a zero normal with its status and counts both cases in the report.
PositionError must be at least the surface's position floor. Errors are
measured against the prepared float32 patches; TryPrepareSurface checks source
narrowing separately. Every failure keeps the previous mesh.
Convex polygon partition¶
TryPartitionConvexPolygons turns a triangle mesh, such as an emissive light
mesh, into the convex planar polygons that FoundationMath's
projected solid angle sampling expects:
at most seven vertices, no three collinear, as Peters 2021 requires in section 3.1
and supplement A.2.
#include <ludus/foundation/geometry/convex_polygon.hpp>
ConvexPolygonPartition partition;
if (IsSuccess(TryPartitionConvexPolygons({positions, positionCount}, {indices, indexCount}, partition)))
{
for (const ConvexPolygon& polygon : partition.Polygons)
{
// polygon.PositionsMesh[0 .. VertexCount) wind counterclockwise about polygon.NormalMesh;
// partition.Triangles[FirstTriangle .. FirstTriangle + TriangleCount) are its source triangles.
}
}
Triangles are matched by equal positions, so split vertices along normal or UV seams still share edges. Two pieces merge across a shared edge when the edge has exactly two triangles of opposite direction and the union stays coplanar, convex, simple and within seven vertices once collinear vertices are removed. Merging follows the whole chain of edges the pieces share, so a fan around a center vertex or a tessellated grid can collapse into one polygon. Non-convex patches and patches with holes become several convex polygons, as the paper asks, instead of being rejected. This adapts the inessential-diagonal removal of Hertel and Mehlhorn, "Fast triangulation of simple polygons", FCT 1983, LNCS 158, pp. 207-218; the greedy order is deterministic but the partition is not minimal.
Every nondegenerate triangle belongs to exactly one polygon, and a lone triangle
is a polygon of three vertices. Output positions are bitwise source positions.
Triangles with coincident or collinear corners are counted in
DegenerateTriangleCount and left out; edges with more than two triangles or
inconsistent winding are counted in UnmergeableEdgeCount and never crossed.
Coplanarity and collinearity use tolerances relative to the polygon's extent with
a floor relative to float32 coordinate magnitude; far from the origin, rounding
can keep a fan's center vertex visibly off its chord, leaving several convex
pieces. Invalid views or index counts, out-of-range indices and nonfinite
positions fail without changing the output. The partition is CPU preparation
work; it allocates its scratch and output arrays.
Not yet provided¶
Path cursors for coherent traversal, a Visual-only Estimated distance build, Approximate table lookup, certified frame angular error, mitred or split sweeps, G1 surface classification, camera-dependent and displaced meshes, quadtree tessellation, cooked formats and editor adapters are later work. Performance and memory budgets are unmeasured.
Point neighborhoods¶
point_search.hpp provides PointSearchTree, a deterministic balanced 3D point
tree. Build copies World positions, accepting duplicates and rejecting nonfinite
coordinates, magnitudes above MAX_POINT_SEARCH_COORDINATE, and invalid counts.
Failure preserves the previous tree. Build costs O(n log² n), using Ludus sorting
and fallible containers. FindNearest allocates nothing, returns 1 through
MAX_POINT_NEIGHBORS results, and orders equal distances by original source index.
The requested count cannot exceed the input point count. Query failure preserves
outputs. This is ordinary float32 spatial search, not a certified geometric bound;
nearly equal distances can be ordered differently on different processors.
GetNodes exposes immutable packed nodes for GPU upload. point_search_layout.h
owns the shared CPU/GPU representation and traversal limits. The portable shader
implementation is installed beside the renderer recipes; the
scene preparation guide
explains the GPU query helper and its explicit failure sentinel.