Derive an orthonormal in-plane basis (tangent, bitangent) from a
plane normal. Non-unit input is fine — the normal is normalised here, so
only its direction is read.
Properties guaranteed by the implementation (covered by tests):
tangent · normal ≈ 0 and bitangent · normal ≈ 0.
tangent · bitangent ≈ 0.
|tangent| = |bitangent| = 1.
The result is deterministic — the same normal always yields
the same (tangent, bitangent). This is essential so the cap
hatch doesn't rotate when state is reconstructed (e.g. on reload
or when the renderer rebuilds resources).
Every component of both axes is finite, for every input.
The result is continuous in the normal, everywhere except the two
poles n = ±Y (#2714) — nearby normals give nearby bases, so two
face picks on nearly the same face give nearly the same drawing.
Normalising up front is what makes 3 and 5 true rather than aspirational
(#2489). Before it, the function read the caller's magnitudes directly and
every magnitude test below was therefore a test on length where the
comment claimed an angle:
• A non-finite component sailed through both of them — Infinity > 1e-9
is true and NaN < 1e-9 is false — and reached the divisions as
Infinity / Infinity / NaN / NaN. Both axes came back all-NaN and
went into the section gizmo's vertex buffer and the cap's lift-to-3D.
• The reference-axis pick of the day (|ny| < 0.9, since removed for
being discontinuous) only measures the angle to Y when |normal| = 1.
[10, 1, 0] is 6° off horizontal but was routed down that fallback,
flipping the hatch axis 180° purely because the caller had not
normalised.
• The 1e-9 tangent floor is a length, so a short-but-perfectly-valid
normal such as [1e-12, 0, 0] was declared degenerate and returned a
zero-length bitangent.
Normalisation keeps the sole remaining fallback below an exact test — it is
reachable only for a normal with no direction at all, or one pointing
exactly along ±Y.
Derive an orthonormal in-plane basis (
tangent,bitangent) from a plane normal. Non-unit input is fine — the normal is normalised here, so only its direction is read.Properties guaranteed by the implementation (covered by tests):
tangent · normal ≈ 0andbitangent · normal ≈ 0.tangent · bitangent ≈ 0.|tangent| = |bitangent| = 1.normalalways yields the same(tangent, bitangent). This is essential so the cap hatch doesn't rotate when state is reconstructed (e.g. on reload or when the renderer rebuilds resources).n = ±Y(#2714) — nearby normals give nearby bases, so two face picks on nearly the same face give nearly the same drawing.Normalising up front is what makes 3 and 5 true rather than aspirational (#2489). Before it, the function read the caller's magnitudes directly and every magnitude test below was therefore a test on length where the comment claimed an angle: • A non-finite component sailed through both of them —
Infinity > 1e-9is true andNaN < 1e-9is false — and reached the divisions asInfinity / Infinity/NaN / NaN. Both axes came back all-NaN and went into the section gizmo's vertex buffer and the cap's lift-to-3D. • The reference-axis pick of the day (|ny| < 0.9, since removed for being discontinuous) only measures the angle to Y when|normal| = 1.[10, 1, 0]is 6° off horizontal but was routed down that fallback, flipping the hatch axis 180° purely because the caller had not normalised. • The1e-9tangent floor is a length, so a short-but-perfectly-valid normal such as[1e-12, 0, 0]was declared degenerate and returned a zero-length bitangent. Normalisation keeps the sole remaining fallback below an exact test — it is reachable only for a normal with no direction at all, or one pointing exactly along ±Y.