From 75c3fbba77daf1667067e8db8db9285bb1d8e774 Mon Sep 17 00:00:00 2001 From: alowpoly Date: Mon, 5 Oct 2026 14:50:05 -0300 Subject: [PATCH] fix(universe): a moons view relaxes the dimming of orbits outside its family as the camera pulls back --- docs/universe-labels.md | 2 +- .../src/universe/context-presentation-policy.test.ts | 9 ++++----- .../renderer/src/universe/context-presentation-policy.ts | 9 +++++---- .../src/universe/world-context/world-context-planner.ts | 2 +- .../world-context/world-context-system-selection.test.ts | 8 +++++--- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/universe-labels.md b/docs/universe-labels.md index ab8c6ca480e..8f50fad34a0 100644 --- a/docs/universe-labels.md +++ b/docs/universe-labels.md @@ -85,7 +85,7 @@ A path's opacity follows from that: | Any path in an overview, or one the reader points at | Softens to 30% up close; never dimmed by family. | The outside dimming eases away between half and twice the host's distance from -its star as the camera pulls back; a moons view holds it. Viewport clipping, +its star as the camera pulls back, in a moons view too. Viewport clipping, body occlusion, the system's distance fade, the on-screen size fade and explicit hidden-orbit settings still apply. diff --git a/packages/renderer/src/universe/context-presentation-policy.test.ts b/packages/renderer/src/universe/context-presentation-policy.test.ts index e2f86841621..a5719ce76f2 100644 --- a/packages/renderer/src/universe/context-presentation-policy.test.ts +++ b/packages/renderer/src/universe/context-presentation-policy.test.ts @@ -59,14 +59,13 @@ test('the close-up fades keep a floor for context and none for the focused body\ assert.deepEqual(closeOrbitFades(fade, .5), { context: .3, own: 0 }); }); -test('the dimming outside a family relaxes as the camera reaches the parent system, and a moons view holds it', () => { +test('the dimming outside a family relaxes as the camera reaches the parent system', () => { const host = { positionM: [10, 0, 0], orbit: { centerPositionM: [0, 0, 0] } }; - const at = (distance: number, held = false) => outsideFamilyOrbitOpacity(host, () => distance, held); + const at = (distance: number) => outsideFamilyOrbitOpacity(host, () => distance); assert.deepEqual([at(1), at(5), at(20), at(100)], [.25, .25, 1, 1]); assert.ok(at(10) > .25 && at(10) < 1); - assert.equal(at(100, true), .25); - assert.equal(outsideFamilyOrbitOpacity(undefined, () => 1, false), 1); - assert.equal(outsideFamilyOrbitOpacity({ positionM: [0, 0, 0] }, () => 1, false), 1); + assert.equal(outsideFamilyOrbitOpacity(undefined, () => 1), 1); + assert.equal(outsideFamilyOrbitOpacity({ positionM: [0, 0, 0] }, () => 1), 1); }); test('a moon is named inside the kept families, and beside a planet or a moon its star\'s minor bodies are not', () => { diff --git a/packages/renderer/src/universe/context-presentation-policy.ts b/packages/renderer/src/universe/context-presentation-policy.ts index b8b90066ff5..3fa81ac26af 100644 --- a/packages/renderer/src/universe/context-presentation-policy.ts +++ b/packages/renderer/src/universe/context-presentation-policy.ts @@ -28,7 +28,8 @@ export interface ContextSubject { readonly families: ReadonlySet; /** A selected body's own page, with no flight: a host's page fades its moons' paths with the host. */ readonly page: boolean; - /** A moons view is selected: the dimming outside the family holds at every range, the host's own path included. */ + /** A moons view is selected: it holds its host as the subject, and the dimming outside the family includes the host's + * own path. */ readonly held: boolean; /** Beside the page of a body that is not a system's star, that star's minor bodies are not named. */ readonly quietMinors: boolean; @@ -69,13 +70,13 @@ export function closeOrbitFades(fade: OrbitLineFade, discHeightShare: number) { /** An orbit belongs to the family it circles, not the body travelling on it, so a path outside the subject's family is * dim context: the other planets', and the host's own path about its star when the subject is not the host itself. * `host` is the family's host, absent without an emphasised body or when it is a star. The dimming relaxes from half to - * twice the host's distance from its star, where the subject is the parent system again; a moons view holds it. + * twice the host's distance from its star, where the view is the parent system again; a moons view relaxes it too. * 1/64 steps avoid rewriting opacity for tiny camera changes. */ export function outsideFamilyOrbitOpacity( - host: Host | undefined, cameraDistanceM: (host: Host) => number, held: boolean): number { + host: Host | undefined, cameraDistanceM: (host: Host) => number): number { if (!host?.orbit) return 1; const radiusM = Math.hypot(...host.positionM.map((value, axis) => value - host.orbit!.centerPositionM[axis]!)); - const t = held || !(radiusM > 0) ? 0 : Math.max(0, Math.min(1, Math.log2(cameraDistanceM(host) / (.5 * radiusM)) / 2)); + const t = !(radiusM > 0) ? 0 : Math.max(0, Math.min(1, Math.log2(cameraDistanceM(host) / (.5 * radiusM)) / 2)); return 1 - (1 - OUTSIDE_FAMILY_OPACITY) * Math.round((1 - t * t * (3 - 2 * t)) * 64) / 64; } diff --git a/packages/renderer/src/universe/world-context/world-context-planner.ts b/packages/renderer/src/universe/world-context/world-context-planner.ts index 7c4af75820b..9be8e4d8818 100644 --- a/packages/renderer/src/universe/world-context/world-context-planner.ts +++ b/packages/renderer/src/universe/world-context/world-context-planner.ts @@ -191,7 +191,7 @@ export function createWorldContextPlanner(plan: PreparedWorldContext | PreparedW Math.abs(selectedY) < height / 2 + focusDiameter / 2; const focusShare = focusInView ? focusDiameter / height : 0; const fades = closeOrbitFades(plan.camera.presentation.orbitLineFade, focusShare); - const outsideFamily = outsideFamilyOrbitOpacity(subject.id === null || subject.hostIsStar ? undefined : byId.get(subject.hostId), host => Math.hypot(...frame.eye(host)), subject.held); + const outsideFamily = outsideFamilyOrbitOpacity(subject.id === null || subject.hostIsStar ? undefined : byId.get(subject.hostId), host => Math.hypot(...frame.eye(host))); const highlighting = bodies.some(entry => entry.highlighted === true), otherSystems = otherSystemsOpacity(systemFade.of(0), starFieldFade(focusDistanceM, plan.system)); const near = opacity > 0 ? Math.max(1, Math.min(...bodies.map(entry => Math.hypot(...frame.eye(entry.body)))) * 0.01) : 1; diff --git a/packages/renderer/src/universe/world-context/world-context-system-selection.test.ts b/packages/renderer/src/universe/world-context/world-context-system-selection.test.ts index d00332e7963..90f5c1574f9 100644 --- a/packages/renderer/src/universe/world-context/world-context-system-selection.test.ts +++ b/packages/renderer/src/universe/world-context/world-context-system-selection.test.ts @@ -35,11 +35,13 @@ test('a selected satellite overview retains its selected host locator', () => { const close = orbits(view), closePlain = orbits({ ...view, overviewSelection: false }); const dimmed = orbits(wide), plain = orbits({ ...wide, overviewSelection: false }); const hovered = orbits({ ...wide, bodies: wide.bodies.map((body, at) => at === index('saturn') ? { ...body, hovered: true } : body) }); - for (const id of ['saturn', 'jupiter']) assert.deepEqual([dimmed.get(id), plain.get(id), hovered.get(id)], [.25, 1, id === 'saturn' ? 1 : .25], id); + assert.equal(close.get('jupiter'), .25); // On the host's own page its path is the subject's and stays whole; the other planets' paths are still context. - const page = orbits({ ...wide, overview: false, overviewSelection: false }); + const page = orbits({ ...wide, overview: false, overviewSelection: false }), relaxed = page.get('saturn')!; assert.equal(page.get('jupiter'), 1); - assert.ok(page.get('saturn')! >= .25 && page.get('saturn')! < 1); + assert.ok(relaxed > .25 && relaxed < 1); + // Pulled back towards the parent system, the moons view relaxes the dimming as the page does, the host's own path with it. + for (const id of ['saturn', 'jupiter']) assert.deepEqual([dimmed.get(id), plain.get(id), hovered.get(id)], [relaxed, 1, id === 'saturn' ? 1 : relaxed], id); assert.ok(moons.some(id => close.get(id) === 1)); for (const id of moons) assert.equal(close.get(id), closePlain.get(id), id); // One emphasis per body: a highlighted set dims the rest, another star's bodies read as not belonging inside the