File size: 9,448 Bytes
3ce1a40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Repoverse Preview</title>
<!-- Developer preview of generator output (world-mesh.json from `repoverse export-mesh`).
     This checks geometry and manifest data by eye; the shipped renderer is Unreal. -->
<style>
  :root { --bg:#0e1116; --panel:rgba(20,24,31,.86); --line:rgba(255,255,255,.12); --text:#e6e9ef; --muted:#9aa3b2; --accent:#7cc4ff; }
  * { box-sizing: border-box; }
  html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text); font: 13px/1.45 ui-sans-serif, system-ui, sans-serif; overflow: hidden; }
  canvas { display: block; }
  .panel { position: fixed; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; backdrop-filter: blur(8px); }
  #hud { top: 16px; left: 16px; max-width: min(360px, calc(100vw - 32px)); }
  #inspector { top: 16px; right: 16px; width: min(340px, calc(100vw - 32px)); display: none; max-height: calc(100vh - 32px); overflow: auto; }
  h1 { font-size: 14px; margin: 0 0 4px; } h2 { font-size: 14px; margin: 0 0 2px; word-break: break-all; }
  .muted { color: var(--muted); } .why { color: var(--muted); font-size: 11px; }
  label { display: inline-flex; gap: 6px; align-items: center; margin-right: 12px; cursor: pointer; }
  dl { display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; margin: 8px 0; } dt { color: var(--muted); } dd { margin: 0; }
  ul { margin: 6px 0 0; padding-left: 16px; } li { margin: 1px 0; }
  @media (max-width: 640px) { #inspector { top: auto; bottom: 16px; max-height: 45vh; } }
</style>
<script type="importmap">{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.169.0/examples/jsm/" } }</script>
</head>
<body>
<div id="hud" class="panel">
  <h1>Repoverse · generator preview</h1>
  <div id="stats" class="muted">loading world-mesh.json…</div>
  <div style="margin-top:8px">
    <label><input type="checkbox" id="roofs" checked> roofs</label>
    <label><input type="checkbox" id="objects" checked> source objects</label>
  </div>
  <div class="muted" style="margin-top:6px">drag to orbit · scroll to zoom · click a building</div>
</div>
<div id="inspector" class="panel"></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// Material-family base colours, lightened per block role so walls, floors and windows read apart.
const FAMILY = { stone:0x9a948a, concrete:0xb8bcc2, glass:0x8fc7e8, metal:0x8c96a3, wood:0xb88a5a, ceramic:0xe2d6c4,
  electronics:0x5e7b8f, laboratory:0xdfe7ec, archive:0xc9a86b, security:0x5b6275 };
const BLOCK = { grass:0x5f8f4e, road:0x3b3f46, sidewalk:0x9fa3a8, soil:0x6b5237, bedrock:0x333333, foundation:0x7f7a72,
  ladder:0xd9a441, scaffold:0xe0b030 };
const OBJECT = { SourceTerminal:0x7cc4ff, TestBench:0x7de3a1, ProofVault:0xc39bff, ArchiveShelf:0xe8c47c, BuildStation:0xff9d6b,
  DataRack:0x5fd4d0, ConfigPanel:0xb0b8c4, AssetCrate:0xd28ac7, NotebookDesk:0xf2e28a, HardwareBench:0xff6b6b };

const colorFor = (block, family) => {
  const b = block.toLowerCase(), f = family.toLowerCase();
  if (BLOCK[b] !== undefined) return new THREE.Color(BLOCK[b]);
  const c = new THREE.Color(FAMILY[f] ?? 0xaaaaaa);
  if (b === 'window') return c.lerp(new THREE.Color(0x9fd8ff), .6);
  if (b === 'roof') return c.multiplyScalar(.72);
  if (b === 'floor') return c.multiplyScalar(.85);
  if (b === 'partition') return c.lerp(new THREE.Color(0xffffff), .3);
  return c;
};

const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xbfd6e6);
scene.fog = new THREE.Fog(0xbfd6e6, 900, 3200);
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 1, 8000);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
scene.add(new THREE.HemisphereLight(0xffffff, 0x6b5a45, 1.1));
const sun = new THREE.DirectionalLight(0xffffff, 1.6); sun.position.set(.6, 1, .35); scene.add(sun);

// Terrain outside exported chunks: a plane just below the ground surface (y = GroundLevel = 8).
const ground = new THREE.Mesh(new THREE.PlaneGeometry(20000, 20000), new THREE.MeshLambertMaterial({ color: 0x5f8f4e }));
ground.rotation.x = -Math.PI / 2; ground.position.y = 7.9; scene.add(ground);

const world = await (await fetch(new URLSearchParams(location.search).get('data') ?? 'world-mesh.json')).json();
const roofMeshes = [];
let quads = 0;
const NORMALS = [null, [1,0,0], [0,1,0], [0,0,1]];
for (const g of world.groups) {
  const n = g.normals.length; quads += n;
  const pos = new Float32Array(g.positions), nor = new Float32Array(n * 12), idx = new Uint32Array(n * 6);
  for (let q = 0; q < n; q++) {
    const s = Math.sign(g.normals[q]), v = NORMALS[Math.abs(g.normals[q])];
    for (let k = 0; k < 4; k++) nor.set([v[0]*s, v[1]*s, v[2]*s], (q*4+k)*3);
    idx.set([q*4, q*4+1, q*4+2, q*4, q*4+2, q*4+3], q*6);
  }
  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  geo.setAttribute('normal', new THREE.BufferAttribute(nor, 3));
  geo.setIndex(new THREE.BufferAttribute(idx, 1));
  const glass = g.block === 'Window';
  const mesh = new THREE.Mesh(geo, new THREE.MeshLambertMaterial({ color: colorFor(g.block, g.material), transparent: glass, opacity: glass ? .55 : 1 }));
  scene.add(mesh);
  if (g.block === 'Roof') roofMeshes.push(mesh);
}

const box = new THREE.BoxGeometry(.8, .8, .8);
const objects = new THREE.InstancedMesh(box, new THREE.MeshLambertMaterial(), world.objects.length);
world.objects.forEach((o, i) => {
  objects.setMatrixAt(i, new THREE.Matrix4().makeTranslation(o.x + .5, o.y + .4, o.z + .5));
  objects.setColorAt(i, new THREE.Color(OBJECT[o.kind] ?? 0xffffff));
});
scene.add(objects);

// Invisible pick volumes, one per building.
const pickables = world.buildings.map(b => {
  const m = new THREE.Mesh(new THREE.BoxGeometry(b.width, b.height, b.depth), new THREE.MeshBasicMaterial({ visible: false }));
  m.position.set(b.x + b.width / 2, 8 + b.height / 2, b.z + b.depth / 2);
  m.userData = b; scene.add(m); return m;
});

const bounds = new THREE.Box3();
pickables.forEach(p => bounds.expandByObject(p));
const center = bounds.getCenter(new THREE.Vector3()), size = bounds.getSize(new THREE.Vector3()).length();
controls.target.copy(center);
camera.position.copy(center).add(new THREE.Vector3(size * .35, size * .45, size * .55));

document.getElementById('stats').textContent =
  `${world.buildings.length} buildings · ${quads.toLocaleString()} quads · ${world.groups.length} material groups · ${world.objects.length} source objects · seed ${world.worldSeed}`;
document.getElementById('roofs').onchange = e => roofMeshes.forEach(m => m.visible = e.target.checked);
document.getElementById('objects').onchange = e => objects.visible = e.target.checked;

function inspect(b, target) {
  const el = document.getElementById('inspector');
  el.style.display = 'block';
  el.innerHTML = `<h2>${esc(b.repo)}</h2><div class="muted">${esc(b.description)}</div>
    <dl><dt>category</dt><dd>${esc(b.category)}<div class="why">${esc(b.categoryWhy)}</div></dd>
    <dt>district</dt><dd>${esc(b.district)}</dd><dt>archetype</dt><dd>${esc(b.archetype)}</dd>
    <dt>floors</dt><dd>${b.floors}</dd><dt>activity</dt><dd>${esc(b.activity)}</dd><dt>state</dt><dd>${esc(b.construction)}</dd></dl>
    <div class="muted">rooms</div><ul>${b.rooms.map(r => `<li>${esc(r.name)} <span class="muted">· ${esc(r.purpose)} · floor ${r.floor} · ${r.fileCount} files</span></li>`).join('')}</ul>`;
  controls.target.copy(target);
}

const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;' }[c]));
const ray = new THREE.Raycaster(), ptr = new THREE.Vector2();
let down = null;
renderer.domElement.addEventListener('pointerdown', e => down = [e.clientX, e.clientY]);
renderer.domElement.addEventListener('pointerup', e => {
  if (!down || Math.hypot(e.clientX - down[0], e.clientY - down[1]) > 4) return;
  ptr.set(e.clientX / innerWidth * 2 - 1, -e.clientY / innerHeight * 2 + 1);
  ray.setFromCamera(ptr, camera);
  const hit = ray.intersectObjects(pickables)[0];
  const el = document.getElementById('inspector');
  if (!hit) { el.style.display = 'none'; return; }
  inspect(hit.object.userData, hit.object.position);
});

// ?focus=owner/repo frames one building and opens its inspector (deep link, §76).
const focus = new URLSearchParams(location.search).get('focus');
const focused = pickables.find(p => p.userData.repo === focus?.toLowerCase());
if (focused) {
  const b = focused.userData;
  inspect(b, focused.position);
  camera.position.copy(focused.position).add(new THREE.Vector3(b.width * 1.1, b.height + 34, b.depth * 1.6));
}

addEventListener('resize', () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); });
renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); });
window.__ready = true;
</script>
</body>
</html>