Merge pull request #25 from n30nex/codex/netgraph-camera-orientation-20261011

Fix inverted 3D Netgraph perspective (0.7.1)
This commit is contained in:
n30nex authored and GitHub committed 2026-10-11 20:19:21 -04:00
commit fc27d07344
9 files changed
+38 -12

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Copy this file to .env and configure your MQTT feed. No map key or build is needed.
# Keep .env and any optional .secrets files private.
CARTOLITE_IMAGE=ghcr.io/n30nex/cartolite-server:0.7.0
CARTOLITE_IMAGE=ghcr.io/n30nex/cartolite-server:0.7.1
CARTOLITE_BIND_ADDR=127.0.0.1
CARTOLITE_PORT=8080
# Optional CARTO override: see compose.carto.yml and docs/deployment.md.
+2 -2
View File
@@ -15,8 +15,8 @@ concurrency:
env:
IMAGE_NAME: ghcr.io/n30nex/cartolite-server
# Exact published 0.6.4, in addition to the legacy 0.4.1 source migration below.
UPGRADE_FROM_IMAGE: ghcr.io/n30nex/cartolite-server@sha256:aac2452b05f6de3a79404e3d2ea807d99543ef38ee67e63a478eb6914d7e8a97
# Exact published 0.7.0, in addition to the legacy 0.4.1 source migration below.
UPGRADE_FROM_IMAGE: ghcr.io/n30nex/cartolite-server@sha256:33d8a85d5052f7fd220845b0f46d5873b627c6507bd8721bc83713872328b41d
jobs:
backend:
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## 0.7.1 - Correct 3D Netgraph camera orientation
- Correct the reversed perspective depth so 3D looks down onto the scene: nearby groups appear larger and raised connections sit above the ground.
- Keep panning, pointer anchoring, route picking and packet effects aligned with the corrected camera; top-down mode is unchanged.
- Add physical camera-orientation regressions alongside the existing projection, gesture, mobile and scale checks.
## 0.7.0 - 3D Netgraph and connection inspection
- Start top-down, with a 3D button for raised geographic groups, arched links and depth-aware live packet effects.
+1 -1
View File
@@ -1 +1 @@
0.7.0
0.7.1
+1 -1
View File
@@ -2,7 +2,7 @@ name: cartolite-server
services:
cartolite:
image: ${CARTOLITE_IMAGE:-ghcr.io/n30nex/cartolite-server:0.7.0}
image: ${CARTOLITE_IMAGE:-ghcr.io/n30nex/cartolite-server:0.7.1}
restart: unless-stopped
ports:
- "${CARTOLITE_BIND_ADDR:-127.0.0.1}:${CARTOLITE_PORT:-8080}:8080"
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "cartolite-server-web",
"version": "0.7.0",
"version": "0.7.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cartolite-server-web",
"version": "0.7.0",
"version": "0.7.1",
"dependencies": {
"maplibre-gl": "6.4.1"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cartolite-server-web",
"version": "0.7.0",
"version": "0.7.1",
"private": true,
"type": "module",
"scripts": {
+19 -1
View File
@@ -13,6 +13,24 @@ describe('Netgraph shared 3D geometry', () => {
}
});
it('views the ground from above: foreground is larger and raised links rise toward the camera', () => {
for (const tilt of [Math.PI / 6, GRAPH_TILT, 1.1]) for (const yaw of [-2.8, 0, 1.4]) {
const p = new GraphProjection({ ...camera, x: 0, y: 0, zoom: 1, tilt, yaw }, 1200, 800);
const point = (y: number, z = 0) => ({ x: y * Math.sin(yaw), y: y * Math.cos(yaw), z });
const front = p.project(point(600)), back = p.project(point(-600));
expect(front.scale).toBeGreaterThan(1); expect(back.scale).toBeLessThan(1);
expect(front.y).toBeGreaterThan(400); expect(back.y).toBeLessThan(400);
for (const y of [-600, 0, 600]) {
expect(p.project(point(y, 100)).y).toBeLessThan(p.project(point(y)).y);
}
// A point lifted along the viewing ray stays centered and gets nearer.
const alongRay = p.project(point(300 * Math.sin(tilt), 300 * Math.cos(tilt)));
expect(alongRay.x).toBeCloseTo(600); expect(alongRay.y).toBeCloseTo(400);
expect(alongRay.depth).toBeCloseTo(-300);
expect(alongRay.scale).toBeCloseTo(camera.distance / (camera.distance - 300));
}
});
it('fits neighbours and arc crests into the space left by desktop and mobile inspectors', () => {
const a = { x: -450, y: -100, z: 18 }, b = { x: 900, y: 450, z: 28 };
const points = [a, b, graphArcPoint(a, b, 0.5, true)];
@@ -47,7 +65,7 @@ describe('Netgraph shared 3D geometry', () => {
it('clips behind-camera points without drawing or picking a line across the gap', () => {
const projection = new GraphProjection({ ...camera, tilt: 1, distance: 1000 }, 1200, 800);
expect(projection.project({ x: 0, y: -5000 }).visible).toBe(false);
expect(projection.project({ x: 0, y: 5000 }).visible).toBe(false);
const broken = [{ x: 0, y: 0, progress: 0 }, { x: 100, y: 100, progress: 0.5, breakBefore: true }, { x: 110, y: 110, progress: 1 }];
expect(distanceToGraphPath(broken, 50, 50)).toBeGreaterThan(50);
expect(graphPathPoint(broken, 0.25).x).toBeLessThan(0);
+5 -3
View File
@@ -25,7 +25,9 @@ export class GraphProjection {
const dx = point.x - camera.x, dy = point.y - camera.y;
const x = dx * this.cy - dy * this.sy, y = dx * this.sy + dy * this.cy;
const z = camera.tilt === 0 ? 0 : (point.z ?? 0);
const depth = y * this.st - z * this.ct;
// The camera sits above the foreground (+y, +z). Screen-down and
// depth must be orthogonal; foreground objects are nearer, not farther.
const depth = -y * this.st - z * this.ct;
const denominator = camera.distance + depth;
if (!Number.isFinite(denominator) || denominator < camera.distance * 0.08) return hidden();
const scale = camera.tilt === 0 ? 1 : camera.distance / denominator;
@@ -38,9 +40,9 @@ export class GraphProjection {
ground(x: number, y: number): GraphPoint {
const u = (x - this.width / 2) / this.camera.zoom;
const v = (y - this.height / 2) / this.camera.zoom;
const denominator = Math.max(0.08, this.ct - v * this.st / this.camera.distance);
const denominator = Math.max(0.08, this.ct + v * this.st / this.camera.distance);
const ry = v / denominator;
const rx = u * (1 + ry * this.st / this.camera.distance);
const rx = u * (1 - ry * this.st / this.camera.distance);
return { x: this.camera.x + rx * this.cy + ry * this.sy,
y: this.camera.y - rx * this.sy + ry * this.cy };
}