Add City mode: codebase laid out as streets and buildings
city.js lays the directory tree out at human scale: top-level folders become districts separated by 18 m avenues, nested folders become blocks with narrower streets and raised pavements, and files become buildings whose footprint follows their lines and whose height follows their complexity. The squarified treemap moves into city.js and is shared with the landscape. The renderer gains a second instance layer with per-instance kind, code-atlas rectangle and selection flag, a helicopter camera in metres, screen-to-world rays, and geometry lookup per mode. City mode supports orbit, pan, dolly, ray picking, focusing a building and district names. Node tests cover layout invariants, heights, ray/box hits and the spatial index.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { perspective, lookAt, multiply, transform, orbitPose, ORBIT_FOV } from '../web/camera.js';
|
||||
import { perspective, lookAt, multiply, transform, invert, orbitPose, ORBIT_FOV } from '../web/camera.js';
|
||||
|
||||
// The landscape projection before the real camera, kept as the reference.
|
||||
function legacyProject(camera, [x, y, z], width, height) {
|
||||
@@ -26,3 +26,9 @@ test('orbit camera reproduces the legacy landscape projection', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('invert undoes a view-projection', () => {
|
||||
const m = multiply(perspective(1, 1.5, .1, 1000), lookAt([3, 4, 5], [0, 0, 0]));
|
||||
const inv = invert(m), id = multiply(m, inv);
|
||||
for (let i = 0; i < 16; i++) assert.ok(Math.abs(id[i] - (i % 5 === 0 ? 1 : 0)) < 1e-4, `${i}: ${id[i]}`);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { layoutCity, buildingHeight, spatialIndex, rayBox, pickRay, CITY } from '../web/city.js';
|
||||
|
||||
function sampleFiles() {
|
||||
const files = []; let id = 1;
|
||||
for (const dir of ['src/core', 'src/ui/widgets', 'tests', 'docs', 'vendor/lib', '']) {
|
||||
for (let i = 0; i < 12; i++) files.push({ id: id++, path: `${dir ? dir + '/' : ''}file${i}.js`, lines: 20 + (i * 137) % 900, complexity: i * 3 });
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const overlaps = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
||||
|
||||
test('every file gets a building inside the city, and buildings never overlap', () => {
|
||||
const files = sampleFiles(), city = layoutCity(files, 'demo');
|
||||
assert.equal(city.buildings.size, files.length);
|
||||
const list = [...city.buildings.values()];
|
||||
for (const b of list) {
|
||||
assert.ok(b.w > 0 && b.h > 0 && b.x >= 0 && b.y >= 0 && b.x + b.w <= city.width + 1e-6 && b.y + b.h <= city.height + 1e-6, JSON.stringify(b));
|
||||
}
|
||||
for (let i = 0; i < list.length; i++) for (let j = i + 1; j < list.length; j++) assert.ok(!overlaps(list[i], list[j]), `${i} overlaps ${j}`);
|
||||
});
|
||||
|
||||
test('top-level districts are separated by an avenue', () => {
|
||||
const city = layoutCity(sampleFiles(), 'demo');
|
||||
const districts = city.blocks.filter(block => block.depth === 1);
|
||||
assert.ok(districts.length >= 4);
|
||||
for (let i = 0; i < districts.length; i++) for (let j = i + 1; j < districts.length; j++) {
|
||||
const a = districts[i], b = districts[j];
|
||||
const gap = Math.max(b.x - (a.x + a.w), a.x - (b.x + b.w), b.y - (a.y + a.h), a.y - (b.y + b.h));
|
||||
assert.ok(gap >= CITY.roadWidths[0] * .99 || gap >= Math.min(a.w, a.h, b.w, b.h) * .39, `gap ${gap}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the city grows with the codebase and is deterministic', () => {
|
||||
const small = layoutCity(sampleFiles().slice(0, 10)), large = layoutCity(sampleFiles());
|
||||
assert.ok(large.width * large.height > small.width * small.height * 3);
|
||||
assert.deepEqual([...layoutCity(sampleFiles()).buildings], [...large.buildings]);
|
||||
});
|
||||
|
||||
test('complex files are taller', () => {
|
||||
assert.ok(buildingHeight({ complexity: 200 }) > buildingHeight({ complexity: 3 }));
|
||||
assert.equal(buildingHeight({}), 6);
|
||||
});
|
||||
|
||||
test('rays hit the nearest box and miss empty space', () => {
|
||||
const near = { x: 10, y: -1, w: 2, h: 2, height: 5 }, far = { x: 30, y: -1, w: 2, h: 2, height: 50 };
|
||||
assert.equal(rayBox([0, 0, 1], [1, 0, 0], near), 10);
|
||||
assert.equal(rayBox([0, 0, 6], [1, 0, 0], near), Infinity);
|
||||
assert.equal(pickRay([0, 0, 1], [1, 0, 0], [far, near]).entry, near);
|
||||
assert.equal(pickRay([0, 0, 1], [-1, 0, 0], [far, near]), null);
|
||||
assert.equal(pickRay([0, 0, 1], [1, 0, 0], [far, near], 5), null);
|
||||
});
|
||||
|
||||
test('spatial index finds boxes near a point', () => {
|
||||
const a = { x: 0, y: 0, w: 5, h: 5 }, b = { x: 200, y: 200, w: 5, h: 5 };
|
||||
const index = spatialIndex([a, b]);
|
||||
assert.deepEqual([...index.near(2, 2, 3)], [a]);
|
||||
assert.deepEqual([...index.near(100, 100, 3)], []);
|
||||
});
|
||||
Reference in New Issue
Block a user