Fix tether visible-arc logic for exact internal tangency

- Treat k <= -1 as "other zone contains this rim" so a level-1 gate
  tether at exactly the level-2 anchor's range no longer draws a spurious
  full circle inside the union.
- Update doc comment to clarify external vs internal tangency cases.
- Add regression tests covering the d + rInner = rOuter boundary case.
- Add spacestation music and surface video assets.
This commit is contained in:
Brian Fertig 2026-09-08 11:24:02 -06:00
parent 5efc5dfe61
commit 92520a0503
4 changed files with 28 additions and 5 deletions

Binary file not shown.

Binary file not shown.

View File

@ -215,6 +215,25 @@ const dist = (x, y, t) => Math.hypot(x - t.x, y - t.y);
return arcs.length === 1 && Math.abs(arcs[0].a1 - arcs[0].a0 - TAU) < 1e-9;
})());
// INTERNAL TANGENCY — the gate case: d + rInner = rOuter EXACTLY (a
// level-1 gate tether placed at exactly the level-2 anchor's range:
// 5120 + 5120 = 10240). The inner rim lies entirely inside the outer
// zone, touching at one point — it contributes nothing to the union
// boundary, so the inner tether draws NO line; the outer stays full.
// Regression: at k == 1 exactly the old branch fell into "no covered
// arc" and drew the whole inner circle as a spurious border inside the
// union (visible in-game and on the map).
const outerT = new Tether('outer', 0, 0, 1); outerT.radius = 10240;
const innerT = new Tether('inner', 5120, 0, 1); innerT.radius = 5120;
const fieldT = [outerT, innerT];
check('internal tangency: inner rim fully covered → no line inside the union',
Tether.visibleArcs(innerT, fieldT).length === 0);
check('internal tangency: outer tether stays a full circle',
(() => {
const arcs = Tether.visibleArcs(outerT, fieldT);
return arcs.length === 1 && Math.abs(arcs[0].a1 - arcs[0].a0 - TAU) < 1e-9;
})());
// Partial overlap — EXACT covered length (k from the circle geometry):
// covered(θ) where cos(θ−β) ≥ k, k = (rA²+d²rB²)/(2·rA·d)
const A = new Tether('a', 0, 0, 1); // r = 5120

View File

@ -103,9 +103,13 @@ export class Tether {
* another zone o |P o| o.radius cos(θ β) k, where
* β = atan2(o.y y, o.x x), k = ( + o.radius²) / (2·r·d),
* d = |center o|. So each other tether covers the angular interval
* [β arccos k, β + arccos k] (when 1 < k < 1); k < 1 means o
* contains this whole rim; k 1 means no overlap. Union the covered
* intervals on the circle; the complement is the visible arcs.
* [β arccos k, β + arccos k] (when 1 < k < 1); k 1 means o
* contains this whole rim even at EXACT internal tangency (k = 1,
* e.g. a level-1 gate tether placed at exactly the level-2 anchor's
* range: 5120 + 5120 = 10240), where the rim lies entirely inside o
* and contributes no boundary; k 1 means externally tangent or
* disjoint (no covered arc). Union the covered intervals on the
* circle; the complement is the visible arcs.
*
* @returns {Array<{a0:number, a1:number}>} arcs in [0, TAU), a1 > a0, sorted
*/
@ -123,8 +127,8 @@ export class Tether {
continue;
}
const k = (r * r + d * d - o.radius * o.radius) / (2 * r * d);
if (k < -1) return []; // o contains this disc → the whole rim is covered
if (k <= -1 || k >= 1) continue; // tangent or disjoint → no covered arc
if (k <= -1) return []; // o contains this rim — strictly, or exactly internally tangent
if (k >= 1) continue; // externally tangent or disjoint → no covered arc
const beta = Math.atan2(dy, dx);
const half = Math.acos(k);
covered.push([beta - half, beta + half]);