# Blacklight — Closed Bezier Curve Ownership Regions: Design Notes **Status: design, not yet implemented (2026-10-03).** This describes how `ClosedQuadraticBezierCurve` should divide its interior into triangles that each measure distance to at most two curve pieces, and why that division is exact at every shell thickness. It replaces the straight-skeleton ownership regions the drawable uses today. ## 1. The problem A closed curve is drawn as triangles. Each pixel inside the shape is shell if its distance to the boundary is under the shell thickness `t`, and interior otherwise: shell(p) ⇔ min over all pieces P of dist(p, P) < t A pixel shader can't loop over every piece, so each triangle carries a short list of **candidate** pieces and takes the minimum over those alone. That is exact only if, for every pixel the triangle covers, the candidates include a piece the pixel is nearest to. The current scheme gives each piece one region, from the straight skeleton of the chord polygon, with one candidate. Nearest-ness doesn't follow chords, so it draws wrong wherever a piece bows deeply or two pieces compete for the same pixels (a two-piece curve with a bowing piece is the visible case). ### Constraints 1. **Exact at any thickness.** `t` is `max(pixel radius, percent × scale)`, so in model space it changes with zoom every frame. The decomposition is built once on the CPU and can't depend on `t`. 2. **No per-frame CPU work.** Whatever is built at construction has to serve every frame. 3. **At most two candidates per triangle.** Vertex data and the ps_3_0 instruction budget can carry two pieces at a stretch, not more. 4. **Polygonal regions.** Regions are triangulated, so every region boundary must be a straight line or the curve itself (drawn with Loop-Blinn). 5. **Worst-case geometry.** Unit tests can make any number of pieces touch one inscribed circle, and the scheme has to be exact there too, not just usually. ## 2. The key observation The shell test is a minimum over pieces, not a question of which piece is nearest. Write `cell(P)` for the set of interior points that have a nearest boundary point on piece `P` (cells overlap only where points are equidistant). Then: > A region `R` with candidates `{P, Q}` is exact at **every** thickness if and only if > `R ⊆ cell(P) ∪ cell(Q)`. For `p` in `R`, the minimum over `{P, Q}` equals the minimum over all pieces, so the comparison with `t` gives the same answer whatever `t` is. Conversely, if some `p ∈ R` is strictly nearer another piece, any `t` between the two distances gets the wrong answer. Nothing about the thickness enters the construction. The boundaries between cells form the **medial axis**, which is curved (bisectors of parabolic arcs are high-degree algebraic curves). A partition that cut along the medial axis would have curved edges that can't be triangulated exactly. The rest of this document shows how to meet the requirement above with **straight cuts that never follow the medial axis**. ### 2.1 Candidates are whole pieces The candidates are the **whole pieces** `P` and `Q`, not the parts of them that face the region: - **Exactness doesn't depend on where the cuts land.** The criterion is about whole cells, so a cut only has to lie somewhere inside the cell both its neighbours share (Section 6). If the candidates were sub-arcs ending at a cut, a cut computed slightly off would measure pixels just across it to the sub-arc's endpoint instead of to the curve beyond. The error is second order, but not zero. - **Fewer cuts.** A region has to end only where its pair changes. Neighbouring parts of the partition with the same pair merge, and a vertex whose feet all lie on two pieces needs no corner at all (Section 4.3). - **The shader is today's.** Production already measures whole pieces with HalleyTwo, straddling the parabola's vertex where needed, and the benchmarks measure exactly that. The cost is on the GPU. A sub-arc would give a tighter fat line, and HalleyTwo would be likelier to see a single minimum on a shorter arc. If that ever matters, a sub-arc widened past each cut by a margin that covers the cut's error would keep the exactness, but it isn't part of this design. Pieces are still **split** where a region boundary meets them (Section 4.1), because each part of a piece needs its own Loop-Blinn triangle carrying its own region's pair. A split only decides which triangles draw where. It never changes what a candidate measures. ## 3. Medial axis background Let `Ω` be the shape's interior and `∂Ω` its boundary, the union of the pieces. - A disk inside `Ω` is **maximal** if no other disk inside `Ω` contains it. - The **medial axis** `M` is the closure of the set of points with two or more nearest boundary points. Equivalently, it is the set of centres of maximal disks. - Where a maximal disk touches the boundary are its **feet**. - A **medial vertex** is a centre whose maximal disk has three or more separate feet. `M` is a graph of curved **medial edges** joined at vertices. It is a tree, since the curve is a single closed loop. A branch can also end at a **curvature centre**, where a disk osculates one piece. That end has a single foot. **Sites.** The medial axis is described in terms of the boundary's **sites**: each piece, plus each **reflex joint**, a joint whose interior angle is over 180°, which acts as a site of zero length. A reflex joint `J` is the nearest point for a whole fan of directions, between the normals of its two pieces at `J`. Write `∂*Ω` for the boundary with each reflex joint opened up into its fan of directions. Each point of `∂*Ω` has one inward direction: the normal at a point of a piece, or one direction of the fan at a reflex joint. Two facts carry the whole construction. **Lemma 1 (normal segments).** Every point `p ∈ Ω \ M` has exactly one nearest boundary point `q`, and lies on the inward segment from `q` (in the direction of `p`) to the medial axis. Every point of that segment before it reaches `M` has `q` as its unique nearest point. *Proof.* `p ∉ M`, so its nearest point `q` is unique, at distance `d`. The open disk `D(p, d)` contains no boundary point. For `p'` on the open segment from `q` to `p`, the disk `D(p', |p' − q|)` lies inside `D(p, d)` and touches its edge only at `q`, so `q` is the unique nearest point of `p'` as well. Moving outward from `q` in the same direction, `q` stays nearest until the disk first touches a second boundary point or reaches the curvature limit. That point is on `M`, since `M` is a closure. ∎ **Lemma 2 (no crossings).** Inward segments from different points of `∂*Ω` meet only on `M`. *Proof.* A shared point off `M` would have two nearest boundary points, contradicting Lemma 1, or one nearest point in two directions, which is impossible. ∎ So the segments from `∂*Ω` to `M` fill `Ω` without overlapping. Each point off `M` lies on exactly one of them, and that segment starts at the point's nearest boundary point. Write `m(q)` for the point where the segment from `q ∈ ∂*Ω` meets `M`. As `q` moves along `∂*Ω`, `m(q)` moves continuously along `M`. This is a standard property of piecewise smooth boundaries, stated here rather than proven, and opening reflex joints into fans is what makes it hold there too. `m(q)` can pass from one medial edge to another only through a vertex, and only when `q` is one of that vertex's feet. ## 4. The construction ### 4.1 Labels and regions Every point `q ∈ ∂*Ω` belongs to one site. At `m(q)`, the medial axis is equidistant from `q` and from at least one other boundary point. **Label** `q` with its own piece and the piece that other point lies on. A reflex joint's site stands for either of its two pieces (both contain `J` as an endpoint, so both measure it correctly), chosen to match the neighbouring labels where possible. At a leaf, `m(q)` has no other foot. That happens for a single `q` per leaf, which takes the label of the edge the leaf ends. Labels change only: 1. **At the feet of a medial vertex**, where the site across changes. Only some of these change the label (4.3). 2. **At a joint**, where `q`'s own piece changes. 3. **At the far foot of a pseudo-vertex**, where the site across changes because the other side has passed a joint (4.4). A **region** is a connected component, **in `Ω`**, of the union of the inward segments whose starting points share a label, together with the medial edges between them. Connectivity has to be taken in `Ω`, not along `∂*Ω`. The two sides of a medial edge lie on different stretches of the boundary, one on each piece, and only their shared label joins them across the edge. Taken along `∂*Ω`, each side would be its own region, and the two would meet along the curved medial edge. ### 4.2 Exactness **Theorem.** A region `R` labelled `{P, Q}` satisfies `R ⊆ cell(P) ∪ cell(Q)`, so the minimum over its two candidates is the boundary distance at every point of `R`. *Proof.* A point of `R` off `M` lies on the inward segment from some `q` labelled `{P, Q}`. By Lemma 1, `q` is its nearest boundary point, and `q` lies on `P` or `Q` (a reflex joint lies on both of its pieces). A point on a medial edge in `R` has its nearest points at the starting points of the segments that meet there, which are again on `P` or `Q`. ∎ The regions tile `Ω` (Lemma 2), and their edges are straight. Apart from the curve itself, drawn by Loop-Blinn, **every cut is a radius of a maximal disk**, from its centre to one of its feet: - a **spoke** from a medial vertex to one of its feet; - the two radii of a pseudo-vertex's disk (4.4). A radius is an inward segment, so it meets the curve at a right angle. **The medial edges run through the middle of the regions and are never part of a boundary.** Nothing about their shape is ever computed. A region labelled with a single piece, `{P, P}` (Section 4.5), can join a neighbouring `{P, Q}` region, since `{P, Q}` contains its candidate. That removes the cut between them. ### 4.3 Medial vertices of any degree A vertex `V` with `N` feet has `N` spokes. Going around the disk, consecutive feet bound a wedge, and the wedge contains the medial edge between the two sites those feet lie on. Each wedge takes the label of its edge, so it has at most two candidates, whatever `N` is. Neighbouring wedges with the same label merge across the spoke between them, so a vertex whose feet span only two pieces needs no corner. At `V` itself every foot is the same distance away, so whichever region rasterizes that pixel gets the right answer. A corner **is** necessary where the feet span three or more pieces that can't be covered by two (counting a reflex joint as either of its pieces). Every neighbourhood of `V` contains points nearest to each of those three pieces, so a region containing `V` in its interior would need three candidates. Every partition with two candidates per region must have a corner at such a vertex. ### 4.4 Joints Where piece `C` ends and piece `C'` begins at a joint `J`, a medial edge can face `C` and then `C'` on the same side (Section 3's `m` slides past `J`). A single region would need three candidates: the piece across the way, `A`, plus `C` and `C'`. Follow the inward segment from `J`. `J` stays nearest along it until the disk centred on the segment and passing through `J` stops being empty. That happens in one of two ways. **A leaf.** Suppose the tighter of `C` and `C'` curves around the inside with radius `R` at `J`, and the disk reaches radius `R` before touching anything else. At `K = J + R·n` it osculates that piece, and past `K` that piece is nearer than `J`. `K` is a leaf of the medial axis, and a `{C, C'}` edge grows from it, shaped like a convex corner's but starting at `K`. Later that edge meets the rest of the axis at an ordinary vertex with feet on `C`, `C'` and `A`. There is **no cut at `J`**. The segment `J → K` lies inside the `{C, C'}` region, and the corner comes from that vertex's spokes. This is common. Quadratic joints are often G1 but not G2: TrueType's implied on-curve points match tangents, not curvature. Rounded stroke ends and tight bends put the leaf case everywhere. **A pseudo-vertex.** Otherwise the disk first touches the boundary elsewhere, at the point `M_J` where the segment meets the medial axis. `M_J` is nearest only to `J` and to one point `a` on `A`, so it is a **pseudo-vertex**: the centre of a maximal disk with two feet, `J` and `a`. `A`'s side changes label at `a` too. Segments from `A` before `a` end on the stretch of medial axis facing `C`, and those after it end on the stretch facing `C'`. So the cut between `{A, C}` and `{A, C'}` is the bent path `J → M_J → a`, both legs radii of the disk at `M_J`, and `A` is split at `a`. Two regions meet at `M_J`, not three, and both stay exact. Points on the leg from `J` are nearest to `J`, which is an endpoint of both `C` and `C'`. Points on the leg to `a` are nearest to `A`, which both regions test. How the cut looks depends on the joint: - **Smooth joint** (shared tangent): either case, along the shared normal. A leaf needs no cut, and a pseudo-vertex gets the bent cut above. - **Convex corner** (interior angle under 180°): the leaf case with `R = 0`. The leaf is `J` itself, and the edge leaving it faces `C` on one side and `C'` on the other. The corner region is labelled `{C, C'}` and there is no cut. - **Reflex corner** (interior angle over 180°): `J` is its own site (Section 3). Its fan of inward segments is part of `∂*Ω` like any stretch of curve, so the fan can hold medial vertices of its own. A 270° corner facing two different pieces is one example. Sections 4.1–4.3 apply to it unchanged, and every spoke from a vertex in the fan ends at `J`. Where a label changes between `J`'s fan and `C` or `C'`, follow the fan's bounding normal, which is the normal of that piece at `J`. The same two outcomes apply: a leaf needs no cut, and a pseudo-vertex gets the bent cut. ### 4.5 Branches into one piece A sharply curved piece grows a medial branch ending at its centre of curvature, and the edges along that branch face the same piece on both sides. Their label is `{P, P}`: one candidate, which joins the neighbouring region (4.2). A parabola has a single curvature maximum, at its vertex, so each piece adds at most one such `{P, P}` leaf. It does so only if its parameter range includes the vertex, and only if it curves around the inside of the shape there. Leaves also sit off joints (4.4), labelled `{C, C'}`, and those aren't limited by this count. ### 4.6 Two-piece curves A two-piece curve has only two pieces to draw labels from, `P` and `Q`. A reflex joint, like a heart's top dip, stands for either of them. So every label is `{P, Q}` or merges into it, and the whole shape is one region with a zero-area chord polygon. - **A lens**, with both pieces bowing out, is drawn entirely by its two Loop-Blinn triangles. - **A crescent**, with one piece bowing into the shape, is not. The inner piece's control point can lie past the outer curve, so the cut-out polygon `J₁ → J₂ → P1` is neither simple nor inside the region. The crescent works only through Section 5's extra splitting, which brings each inner part's control point back between the curves. No special casing is needed either way, but the crescent depends on that splitting. ## 5. Drawing the regions Each piece is split (exactly, by de Casteljau) at every point where a region boundary meets it, so each **part** of a piece lies in one region. Each region is drawn as: - **Its polygon.** It runs through its vertex and pseudo-vertex corners and its cut points along the curve. A part that bows **out** of the shape contributes its chord. A part that bows **into** the shape contributes the path from one end through its control point `P1` to the other, so that the part's Loop-Blinn triangle is cut out of the polygon, as now. - **Its parts' Loop-Blinn triangles.** For a part bowing out, the shader draws only the pixels between the chord and the curve. For a part bowing in, it draws only the pixels inside the shape between the curve and its control point. Every triangle of a region, polygon or Loop-Blinn, carries that region's two candidate pieces. **What has to hold.** Everything drawn with a pair `{P, P'}` must lie in `cell(P) ∪ cell(P')`, which is Section 2's criterion. Separately, every polygon must be **simple**, which triangulation needs anyway. **What the construction aims for** is the stronger condition that everything drawn with a region's pair lies inside that region. The region's boundary is the curve plus radii, all of which the construction computes, so this is a cheap polygon-containment check. Near each cut it holds automatically. Every cut is a radius of a maximal disk (4.2), perpendicular to the curve where it meets it, and both the part's chord and its control point lie on the part's own side of that line. That includes the far foot of each pseudo-vertex. Further from the cuts it can fail. A deep inward bow can push `P1` past the medial axis or across the opposite chord, and a thin shape can let an outward part's drawn sliver poke across a cut. When it fails: 1. **Certify the triangle against the pair's cells** (Section 7, step 6). A sliver poking into a neighbouring region where `{P, P'}` is still right does no harm. If certification passes, keep it. 2. **Otherwise split the part again.** That moves its control point quadratically closer to the curve, so a finite number of extra splits always restores the stronger condition. The first option needs certification at construction time. If certification ends up test-only (open question 4), only the second applies. ## 6. Numerical robustness The construction is computed in double precision on the CPU, and only one thing in it needs to be precise: **the position of each medial vertex.** - **Away from vertices, cuts have slack.** With whole-piece candidates (2.1), a cut only has to lie inside the cell its two neighbouring regions share. Pixels just either side of it are nearest to the same piece, which both regions test. A spoke computed slightly off still lies inside that cell. - **At a vertex, errors are bounded.** If a vertex is computed with error `δ`, then every point within `δ` of the true vertex is within `O(δ)` of equidistant from all the pieces meeting there, since a difference of two distance functions is 2-Lipschitz and zero at the vertex. Only those points can be assigned a wrong pair, and their distance is off by `O(δ)`. - **Merging is bounded the same way.** Treating two vertices within `ε` of each other as one vertex of higher degree costs at most `O(ε)` in distance. So the merge tolerance is an error bound, not a heuristic. - **So is classifying joints.** Rounding turns a joint meant to be smooth into a tiny corner. Classify every joint within an angle `θ` of 180° as smooth. - **A tiny reflex corner costs nothing.** Its fan is nearest to `J`, an endpoint of both pieces, so whichever side of the cut its points land on, their region measures `J` correctly. It has no shared normal, but the cut can start in any direction inside the fan, such as the bisector, and every one is exact. - **A tiny convex corner costs at most about `θ` times the cut's length.** It would have a short leaf and a sliver region `{C, C'}`. Treating it as smooth puts points within angle `θ` of the true bisector on the wrong side of the cut, where `C` and `C'` differ by at most about `θ` times the distance from `J`. Choose `θ` from the same error budget as the merge tolerance and the certification floor. It also keeps Newton's method away from these near-degenerate inputs. Newton's method on a well-separated vertex converges to about machine precision. On a tangent or nearly coincident configuration it typically gets only about `√ε`, around 1e-8 relative. That is the realistic worst case, and it is still far below the shader's own float rounding. So in practice the scheme is exact, in the same sense as the fat-line early-out's rounding margin: it never changes an answer the float shader could otherwise have got right. ## 7. Computing the medial vertices No library computes medial axes of parabolic arcs, but only the vertices, their feet, the joint cuts and the order of edges between them are needed. Proposed approach: 1. **Seed.** Sample the boundary densely, including each reflex joint as a sample of its own site, and take the Voronoi diagram of the samples. Its vertices whose three generating samples come from three different sites approximate the vertices that may need corners. A vertex with two feet on one site never needs one (4.3), so skipping those is deliberate. The merge in 4.3 later drops corners that turn out unnecessary, such as feet on `P`, `Q` and a reflex joint between them. 2. **Refine.** Solve the tangency system with Newton's method, in a form without square roots. For contacts at parameters `tᵢ` on three pieces, find centre `c`, squared radius `s` and the `tᵢ` with (c − Pᵢ(tᵢ)) · Pᵢ'(tᵢ) = 0, |c − Pᵢ(tᵢ)|² = s, i = 1, 2, 3 That's six polynomial equations in six unknowns. This form also admits disks on the outside of a piece, so check afterwards that `c − Pᵢ(tᵢ)` points inward at each contact. A contact at a reflex joint `J` replaces its pair of equations with `|c − J|² = s`, plus the condition that `c − J` points into the fan at `J`. 3. **Merge.** Vertices within the merge tolerance become one vertex of higher degree, at an error bounded by Section 6. This is where the adversarial tests (`N` pieces tangent to one circle) land. 4. **Check the disks.** Confirm each disk is empty: no boundary point closer to `c` than `√s`. Then build the edge order and the labels. 5. **Joints.** For each smooth joint, and each label change at a reflex fan's edge, grow a disk along the inward ray from `J`, centred on the ray and passing through `J`, until it stops being empty. - If it touches the boundary elsewhere first, its centre is the pseudo-vertex `M_J`. Record that far foot `a`, and split its piece there. - If it first reaches the curvature radius of whichever neighbouring piece curves around the inside more tightly, it ends at a leaf and needs no cut. Convex corners need no cut either (4.4). This step tests emptiness, not equidistance to "some other site". Every point of the ray is equidistant to `J` and to `C'`, which contains `J`, so an equidistance march would run straight past a leaf. Excluding `C` and `C'` wholesale would miss a far part of `C` or `C'` itself. The emptiness test handles both. 6. **Certify what's drawn.** For each triangle drawn with pieces `{P, Q}` (a polygon triangle, or the drawn part of a Loop-Blinn triangle, clipped to the shape), prove that no other piece `X` is nearer than `P` or `Q` anywhere in it. That proves exactly what the shader computes. Exactness is `f_X(p) = dist(p, X) − min(dist(p, P), dist(p, Q)) ≥ 0` on the triangle for every other `X`. - **Quadtree.** `f_X` is 2-Lipschitz, so if `f_X(s) ≥ 2h` at the centre `s` of a cell of radius `h`, then `f_X ≥ 0` on the whole cell. Cover the triangle with an adaptive quadtree, splitting any cell that fails, down to a floor radius `h₀`. Near a vertex where `X` ties, `f_X` reaches zero, so cells there reach the floor. There the check accepts `f_X(s) ≥ −2h₀`, which bounds the error by `4h₀`. The number of cells grows only logarithmically near each vertex. - **Shared ends.** The quadtree can't finish where `X` shares a joint `J` with `P` or `Q`. There `f_X` is zero at every point nearest to `J`, along the whole cut leg from `J` and across a whole reflex fan, and grows only quadratically away from it. So split `X` at a short length `L` from `J` into a **stub** and the rest. The rest stays at least about `L/2` away from points near `J`, so it goes into the quadtree as above. The stub is certified exactly instead. `J` lies on the candidate, so `dist(p, candidate) ≤ |p − J|`, and it suffices that no stub point is nearer to `p` than `J`: |p − X(t)|² ≥ |p − J|² ⇔ (p − J) · (X(t) − J) ≤ |X(t) − J|² / 2 That is linear in `p`, so over a cell clipped to the triangle it suffices to check the clipped polygon's corners. For a quadratic piece starting at `J`, `X(t) − J = t · w(t)` with `w` linear in `t`, so after dividing by `t` each corner `v` leaves the cubic inequality `(v − J) · w(t) ≤ t |w(t)|² / 2` on `(0, L]`. That can be decided exactly from its roots or its Bernstein coefficients. The inequality is sufficient but not necessary. Far from `J`, the candidate's nearest point can be somewhere other than `J`, and the inequality can fail where the stub is still farther. There `f_X` has a positive margin, so the Lipschitz test works on the whole of `X`. A cell passes if **either** the stub inequality **and** the Lipschitz test on the rest of `X` both hold, **or** the Lipschitz test on the whole of `X` holds. Otherwise it is split. Near a joint this needs no floor and no area-sized blowup across a fan, but it isn't free. At depth `d` along a cut leg, the rest of `X` is about `√(d² + L²)` away against a candidate distance of `d`. That's a margin of about `L²/2d`, so cells need `h ≲ L²/4d`. Summed along a leg of length `D`, that's about `2D²/L²` cells, around 2·10⁴ for `L = D/100`. - **What a pass proves.** Every point's two-candidate distance is within `4h₀` of the true distance. Sampling alone could only give evidence. Choosing `h₀` relative to the shape's size, around 1e-7, keeps the bound well under float rounding at any zoom the shape can be drawn at. ### 7.1 Failure and cost - **On failure, construction throws.** If Newton doesn't converge, a disk isn't empty, the labels are inconsistent, or certification fails, either the input is degenerate in a way the tests didn't cover or there's a bug. Both should surface. A curve that silently draws wrong is exactly what this design exists to remove. Resampling more finely around a failure, before giving up, is a possible refinement once real failures have been seen. - **Construction cost is unbudgeted.** Constraint 2 rules out per-frame work, but not curves built at runtime. Shapes are restricted to similarity transforms (Section 9), so one construction serves a glyph at every size, position and rotation, and can be cached per shape. A curve whose control points change pays the full construction each time. The prototype should measure seeding, Newton and certification separately, and certification could become test-only if it dominates and production never fails it. ## 8. GPU side - **The two-curve test is already measured.** In `Blacklight.Bezier`'s TwoCurveTests, `TwoFatLine` gives exactly the same answers as running HalleyTwo on both pieces (`TwoHalley`), in 0.37–0.38 of the time on coherently rendered tiles and 0.98 near the edge. Each candidate is first ruled out by its fat line, and HalleyTwo runs only on candidates the bound can't exclude. - **Vertex data is the tight constraint.** Primitives are drawn as **instanced quads**: one `VertexDefinition` per triangle, shared by all four quad corners, whose only per-vertex input is `CornerIndex`. The vertex shader picks a triangle corner out of the instance data (`TriangleCorner`), so the three corners are **instance data**, not vertex positions. A two-piece region triangle therefore needs 3 corners plus 2 × 3 control points: 18 floats. The pieces share a point when they're boundary neighbours, which brings it to 16. - Available: 12 floats in `UserData_1`–`UserData_3`, plus `TranslationAndVertexPosition.zw`, which triangle types don't use since their corners come from `TriangleCorner`. That's 14. - Options include repacking other fields, a vertex texture fetch of piece data by index (vs_3_0 and WebGL2 can; ps_3_0 can't index), or drawing region triangles outside the instanced path with real per-vertex positions. Undecided. - **Single-candidate regions** use the existing single-piece path. ## 9. Transform restriction The decomposition is fixed in model space, and the shader measures distance in pixel space. Rotation, translation, mirroring and uniform scale preserve nearest-ness. A stretch or shear does not, and under enough anisotropy even the medial axis's topology can change. No partition fixed on the CPU can stay exact under arbitrary non-uniform scale with two candidates per region. So `ClosedQuadraticBezierCurve` (like `Polygon`, whose straight skeleton has the same limitation) accepts only similarity transforms. `PrimitiveBatch` throws if the transform, seen through the camera (`camera.WorldToProjection × transform`), isn't a similarity. A stretch the camera exactly undoes is accepted, since the shader only ever sees the product. Projection space is pixel space, so no aspect-ratio stretch hides in the camera. ## 10. Alternatives considered - **Thickness bands.** Expand each piece's fat line by `t` and overlay the bands, so each region tests the pieces whose bands reach it. The regions are polygonal and exact, but they depend on `t`, which changes every frame. Any number of bands can overlap at one point, so there's no bound on the number of candidates. - **Straight skeleton of the chord polygon** (today's scheme). Chords don't track nearest-ness, so it fails on deep bows and on two-piece curves. - **Measuring the shell in model space**, so that non-uniform scale stays exact. The shell's thickness would then vary with direction under a stretch, which defeats the purpose of a shell. Rejected in favour of Section 9. - **Sub-arcs as candidates.** See 2.1: a tighter shader, but cut errors become distance errors, and every vertex foot needs a cut. ## 11. Open questions 1. Packing two pieces into the vertex data (Section 8). 2. How many extra splits the drawing invariant (Section 5) costs on realistic shapes. 3. Seeding near degeneracies, such as edges much shorter than the sampling spacing. The merge tolerance bounds the error from vertices that nearly coincide (Section 6), and certification catches anything seeding misses, but how finely to resample before giving up is unsettled. 4. The construction-cost budget, and whether certification stays in production (7.1). 5. The error budget that sets the merge tolerance, the joint angle `θ` and the certification floor `h₀` together (Sections 6 and 7). 6. The stub length `L` in certification. A cut leg of length `D` costs about `2D²/L²` cells, so halving `L` quadruples them. A longer stub leaves more of the work to the stub inequality, which only holds where `J` is nearer than any stub point.