ADR 0006 — Profile loops must be correspondence-aligned, not merely equal in count

Status: accepted (2026-08-21) · discovered while prototyping ADR 0001

Context

src/ut_mesh.scad sweeps a region — an outer loop and an inner loop — and caps each end with a ring strip between corresponding points, rather than running a general polygon-with-holes triangulation. That is what keeps the emitter to ~120 lines. It requires len(outer) == len(inner).

Decision

Equal point count is necessary but not sufficient. Invariant PROF-2 additionally requires that both loops be parameterised by the same angular sweep from the same reference, so that outer[i] and inner[i] correspond.

Constructors (ut_round, ut_round(split=)) guarantee it. ut_profile() asserts it.

Evidence

A square bore resampled by perimeter into 32 points satisfies the count rule but starts at a corner (225°) while the circle starts at 0°. The cap strip then twists around the annulus and self-intersects. Rendered result: CGAL error in CGAL_Nef_polyhedron3(): assertion violation! — no useful diagnostic.

Re-parameterising the square by angle (ray-cast from the centre at the same angles as the circle) fixes it: Simple: yes, Volumes 2, and the mesh volume matches the analytic value to six significant figures.

Corollary — both loops are stored counter-clockwise

The original design sketch specified outer-CCW / inner-CW, mirroring how region-based sweep libraries mark a hole. Implementation deviates: both loops are stored CCW.

Reversing the inner loop destroys the very index correspondence this ADR requires, and it would stop ut_bore_rgn() returning a directly usable positive solid — which is what makes the junction two-pass free. The mesh emitter reverses the inner skin’s face winding instead: one line, in one place (src/ut_mesh.scad, _ut_faces_hollow).

Consequences

  • Arbitrary hollow sections (rect duct, D-section, keyed bore) are supported, but their generators must sample by angle, not by arclength.
  • A profile with more than one bore is not supported by the ring-strip cap and needs a real triangulator. Deferred to Phase 5 with that cost stated.
  • This would otherwise have surfaced in Phase 2 as an unexplained CGAL assertion on the first non-circular profile.

This site uses Just the Docs, a documentation theme for Jekyll.