Answer the review: the plan view clipped a system, and pulled its neighbours inward

Two more findings against this projection, both of the same shape as the last
two: something written to the camera that happens to be live, where the plan
view derives from the other one.

The system's own depth range — a near plane a five-hundredth of an
astronomical unit out, a far plane twenty thousand — was set on the active
camera. Entering a system with the plan view already on therefore wrote it to
a camera that re-derives near and far from the perspective one every frame, so
the range never applied and the system clipped. All three unit-space depth
writes go to the perspective camera now, which is the one they are reasoned in.

And the ring of neighbour names collapsed toward the middle of the frame. Its
placement unprojected a point on the ring, treated the offset from the camera
as a direction, and stepped a fixed distance along it — which is a perspective
construction. A parallel projection has no vanishing point to step towards:
every ray through the frame is the view direction, so normalising threw the
sideways part away. Measured before and after, from inside Sol: the two names
sat 319 and 335 pixels out under perspective, 104 and gone under the plan, and
323 and 335 with the unprojected point used as what it already is.

Verified: build clean, 596/596 unit, 16/16 end-to-end on the branch this merges
into, and the ring measured on both projections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
2026-08-21 15:42:37 +02:00
co-authored by Claude Fable 5
parent 8ceea02875
commit e808f50faa
@@ -465,9 +465,13 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
const scene = this.engine.getScene(); const scene = this.engine.getScene();
const camera = this.engine.getCamera(); const camera = this.engine.getCamera();
camera.position.copy(GALAXY_OVERVIEW_POSITION); camera.position.copy(GALAXY_OVERVIEW_POSITION);
camera.near = GALAXY_NEAR_PC; // The perspective camera whichever one is live: it is where the depth range is reasoned,
camera.far = GALAXY_FAR_PC; // and the plan view re-derives its own from it every frame. Writing to the active camera
camera.updateProjectionMatrix(); // put the astronomical-unit range on one that overwrites it, and the system clipped.
const depthCamera = this.engine.getPerspectiveCamera();
depthCamera.near = GALAXY_NEAR_PC;
depthCamera.far = GALAXY_FAR_PC;
depthCamera.updateProjectionMatrix();
this.controls = new OrbitControls(camera, canvas); this.controls = new OrbitControls(camera, canvas);
this.controls.enableDamping = true; this.controls.enableDamping = true;
@@ -902,6 +906,13 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
if (!placed) { if (!placed) {
return null; return null;
} }
if (this.engine.currentProjection === 'orthographic') {
// A parallel projection has no vanishing point to walk towards: every ray through the
// frame is the view direction, so treating the unprojected offset as one and stepping
// along it throws the sideways part away and pulls the whole ring into the middle. The
// unprojected point is already where the name goes.
return this.ringPoint.set(placed.x, placed.y, 0).unproject(camera);
}
const along = this.ringPoint.set(placed.x, placed.y, 0.5).unproject(camera).sub(camera.position).normalize(); const along = this.ringPoint.set(placed.x, placed.y, 0.5).unproject(camera).sub(camera.position).normalize();
return along.multiplyScalar(NEIGHBOUR_DEPTH_AU).add(camera.position); return along.multiplyScalar(NEIGHBOUR_DEPTH_AU).add(camera.position);
} }
@@ -1481,9 +1492,13 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
// clumped over the system's star. // clumped over the system's star.
this.labelOverlay?.update([]); this.labelOverlay?.update([]);
camera.near = SYSTEM_NEAR_AU; // The perspective camera whichever one is live: it is where the depth range is reasoned,
camera.far = SYSTEM_FAR_AU; // and the plan view re-derives its own from it every frame. Writing to the active camera
camera.updateProjectionMatrix(); // put the astronomical-unit range on one that overwrites it, and the system clipped.
const depthCamera = this.engine.getPerspectiveCamera();
depthCamera.near = SYSTEM_NEAR_AU;
depthCamera.far = SYSTEM_FAR_AU;
depthCamera.updateProjectionMatrix();
this.controls!.minDistance = SYSTEM_MIN_DISTANCE_AU; this.controls!.minDistance = SYSTEM_MIN_DISTANCE_AU;
this.controls!.maxDistance = SYSTEM_MAX_DISTANCE_AU; this.controls!.maxDistance = SYSTEM_MAX_DISTANCE_AU;
@@ -1532,9 +1547,13 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
// into the next system entered. // into the next system entered.
this.clearObjectCard(); this.clearObjectCard();
camera.near = GALAXY_NEAR_PC; // The perspective camera whichever one is live: it is where the depth range is reasoned,
camera.far = GALAXY_FAR_PC; // and the plan view re-derives its own from it every frame. Writing to the active camera
camera.updateProjectionMatrix(); // put the astronomical-unit range on one that overwrites it, and the system clipped.
const depthCamera = this.engine.getPerspectiveCamera();
depthCamera.near = GALAXY_NEAR_PC;
depthCamera.far = GALAXY_FAR_PC;
depthCamera.updateProjectionMatrix();
this.controls!.minDistance = GALAXY_MIN_DISTANCE_PC; this.controls!.minDistance = GALAXY_MIN_DISTANCE_PC;
this.controls!.maxDistance = GALAXY_MAX_DISTANCE_PC; this.controls!.maxDistance = GALAXY_MAX_DISTANCE_PC;