@echomem/mcp 1.4.8 → 1.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +23 -3
  2. package/assets/canonical-scorer/README.md +18 -0
  3. package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
  4. package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
  5. package/assets/canonical-scorer/golden_anchors.mjs +83 -0
  6. package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
  7. package/dist/city/chaos-to-clarity-pencil.html +582 -0
  8. package/dist/city/echo-ai-city-only.html +1104 -105
  9. package/dist/city/echo-ai-city-only.template.html +1104 -105
  10. package/dist/city/pencil-pie-generator.html +883 -0
  11. package/dist/city/pencil-webgl-landscape.html +1239 -0
  12. package/dist/city/spatial-fan-story.html +479 -0
  13. package/dist/codex-session-files.js +283 -0
  14. package/dist/codex-sync.js +7 -2
  15. package/dist/context-analysis/canonical-golden.js +47 -0
  16. package/dist/context-analysis/claude-native-canonical.js +1193 -0
  17. package/dist/context-analysis/vendored-canonical.js +793 -0
  18. package/dist/context-analysis/workspace-report.js +1838 -0
  19. package/dist/context-metrics/calculate.js +56 -0
  20. package/dist/context-metrics/model-limits.js +26 -0
  21. package/dist/context-metrics/types.js +1 -0
  22. package/dist/forensics-10-problems.js +7 -6
  23. package/dist/forensics.js +863 -132
  24. package/dist/hud/adapters.js +8 -4
  25. package/dist/hud/metric.js +13 -4
  26. package/dist/hud/monitor.js +135 -16
  27. package/dist/hud/web.js +344 -298
  28. package/dist/index.js +7 -3
  29. package/dist/local-data-paths.js +87 -0
  30. package/dist/migrate.js +37 -29
  31. package/dist/report.js +101 -40
  32. package/dist/setup-page.js +3290 -196
  33. package/dist/setup-preview.js +245 -0
  34. package/dist/setup.js +432 -34
  35. package/package.json +5 -4
  36. package/templates/echomem-recall.md +2 -2
@@ -0,0 +1,1239 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Circular Context City — Pencil WebGL Study</title>
7
+ <style>
8
+ :root {
9
+ --paper: #f3efe5;
10
+ --ink: #30312d;
11
+ --muted: #76736b;
12
+ --codex: #343541;
13
+ --claude: #d97757;
14
+ }
15
+ * { box-sizing: border-box; }
16
+ html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
17
+ body {
18
+ background:
19
+ radial-gradient(circle at 48% 42%, rgba(255,255,255,.98) 0 25%, rgba(249,247,241,.92) 58%, rgba(235,230,218,.82) 100%),
20
+ var(--paper);
21
+ color: var(--ink);
22
+ font-family: "IBM Plex Mono", "SFMono-Regular", ui-monospace, Menlo, monospace;
23
+ }
24
+ body::after {
25
+ content: "";
26
+ position: fixed;
27
+ inset: 0;
28
+ pointer-events: none;
29
+ opacity: .22;
30
+ mix-blend-mode: multiply;
31
+ background-image:
32
+ repeating-linear-gradient(8deg, transparent 0 4px, rgba(64,60,50,.016) 4px 5px),
33
+ repeating-linear-gradient(98deg, transparent 0 9px, rgba(255,255,255,.16) 9px 10px);
34
+ }
35
+ #stage { position: fixed; inset: 0; width: 100%; height: 100%; display: block; cursor: grab; }
36
+ #stage:active { cursor: grabbing; }
37
+ .summary {
38
+ position: fixed;
39
+ left: clamp(20px, 4vw, 54px);
40
+ top: clamp(20px, 4vw, 48px);
41
+ z-index: 3;
42
+ pointer-events: none;
43
+ }
44
+ .eyebrow { font-size: 10px; letter-spacing: .18em; color: var(--muted); text-transform: uppercase; }
45
+ .metric { margin-top: 8px; font: 500 clamp(38px, 5vw, 74px)/.9 Georgia, "Times New Roman", serif; letter-spacing: -.04em; }
46
+ .metric span { font: 500 12px/1.2 "IBM Plex Mono", ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
47
+ .caption { max-width: 360px; margin-top: 12px; font-size: 11px; line-height: 1.55; color: var(--muted); }
48
+ .legend { display: flex; gap: 16px; margin-top: 13px; font-size: 10px; color: var(--muted); }
49
+ .legend span { display: inline-flex; align-items: center; gap: 6px; }
50
+ .legend i { width: 9px; height: 9px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(48,49,45,.14); }
51
+ .legend .codex { background: var(--codex); }
52
+ .legend .claude { background: var(--claude); }
53
+ .actions {
54
+ position: fixed;
55
+ right: clamp(18px, 3vw, 42px);
56
+ bottom: clamp(18px, 3vw, 36px);
57
+ z-index: 4;
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 12px;
61
+ }
62
+ .hint { font-size: 10px; color: var(--muted); }
63
+ button {
64
+ border: 1px solid rgba(48,49,45,.26);
65
+ background: rgba(248,245,237,.84);
66
+ color: var(--ink);
67
+ padding: 8px 12px;
68
+ font: 500 10px/1 "IBM Plex Mono", ui-monospace, monospace;
69
+ letter-spacing: .08em;
70
+ text-transform: uppercase;
71
+ cursor: pointer;
72
+ backdrop-filter: blur(8px);
73
+ }
74
+ button[aria-pressed="true"] { color: #f6f1e6; background: var(--codex); border-color: var(--codex); }
75
+ @media (max-width: 640px) {
76
+ .caption, .hint { display: none; }
77
+ .summary { top: 18px; left: 18px; }
78
+ .actions { right: 16px; bottom: 16px; }
79
+ }
80
+ </style>
81
+ </head>
82
+ <body>
83
+ <canvas id="stage" aria-label="Interactive hand-drawn WebGL city showing repository context morphing from flat graphite pucks into tall frosted-glass columns on a thin paper platform"></canvas>
84
+ <div class="summary" aria-live="polite">
85
+ <div class="eyebrow">Context scan · repository city</div>
86
+ <div class="metric"><b id="dirty-value">82%</b> <span id="dirty-label">dirty context</span></div>
87
+ <div class="caption" id="summary-caption">Dirty context spreads low and wide, carrying graphite, dust and repeated reads.</div>
88
+ <div class="legend" aria-label="Coding agent colors">
89
+ <span><i class="codex"></i>Codex</span>
90
+ <span><i class="claude"></i>Claude Code</span>
91
+ </div>
92
+ </div>
93
+ <div class="actions">
94
+ <span class="hint">Drag to orbit · wheel to zoom</span>
95
+ <button id="dirty" type="button" aria-pressed="true">Dirty</button>
96
+ <button id="repair" type="button" aria-pressed="false">Repair city</button>
97
+ <button id="authentic" type="button" aria-pressed="false">Authentic version</button>
98
+ </div>
99
+
100
+ <script type="importmap">
101
+ { "imports": { "three": "./vendor/three.module.min.js" } }
102
+ </script>
103
+ <script type="module">
104
+ import * as THREE from "three";
105
+ import { OrbitControls } from "./vendor/OrbitControls.js";
106
+ import { RoundedBoxGeometry } from "./vendor/RoundedBoxGeometry.js";
107
+
108
+ const canvas = document.getElementById("stage");
109
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance" });
110
+ renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
111
+ renderer.setClearColor(0xf3efe5, 0);
112
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
113
+ renderer.shadowMap.enabled = true;
114
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
115
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
116
+ renderer.toneMappingExposure = 1.05;
117
+
118
+ const scene = new THREE.Scene();
119
+
120
+ // Soft studio lighting from the earlier clean city: the frosted colour lives
121
+ // inside the glass volume instead of reading as a flat transparent surface.
122
+ function buildStudioEnvironment() {
123
+ const cv = document.createElement("canvas");
124
+ cv.width = 512;
125
+ cv.height = 256;
126
+ const g = cv.getContext("2d");
127
+ const gradient = g.createLinearGradient(0, 0, 0, 256);
128
+ gradient.addColorStop(0, "#ffffff");
129
+ gradient.addColorStop(.5, "#eef1f6");
130
+ gradient.addColorStop(1, "#d2d7e0");
131
+ g.fillStyle = gradient;
132
+ g.fillRect(0, 0, 512, 256);
133
+ const spot = g.createRadialGradient(150, 62, 8, 150, 62, 150);
134
+ spot.addColorStop(0, "rgba(255,255,255,.95)");
135
+ spot.addColorStop(1, "rgba(255,255,255,0)");
136
+ g.fillStyle = spot;
137
+ g.fillRect(0, 0, 512, 256);
138
+ const texture = new THREE.CanvasTexture(cv);
139
+ texture.mapping = THREE.EquirectangularReflectionMapping;
140
+ texture.colorSpace = THREE.SRGBColorSpace;
141
+ const pmrem = new THREE.PMREMGenerator(renderer);
142
+ pmrem.compileEquirectangularShader();
143
+ const environment = pmrem.fromEquirectangular(texture).texture;
144
+ texture.dispose();
145
+ pmrem.dispose();
146
+ return environment;
147
+ }
148
+ scene.environment = buildStudioEnvironment();
149
+ const camera = new THREE.OrthographicCamera(-16, 16, 12, -12, .1, 180);
150
+ camera.position.set(22, 18, 25);
151
+ camera.lookAt(0, 2.3, 0);
152
+
153
+ const controls = new OrbitControls(camera, canvas);
154
+ controls.enableDamping = true;
155
+ controls.dampingFactor = .075;
156
+ controls.enablePan = false;
157
+ controls.minZoom = .72;
158
+ controls.maxZoom = 2.3;
159
+ controls.target.set(0, 2.4, 0);
160
+
161
+ const hemisphere = new THREE.HemisphereLight(0xfffdf7, 0xb8b2a6, .88);
162
+ scene.add(hemisphere);
163
+ const ambient = new THREE.AmbientLight(0xffffff, .3);
164
+ scene.add(ambient);
165
+ const key = new THREE.DirectionalLight(0xfffdf7, 1.28);
166
+ key.position.set(-12, 24, 16);
167
+ key.castShadow = true;
168
+ key.shadow.mapSize.set(2048, 2048);
169
+ key.shadow.camera.left = -22;
170
+ key.shadow.camera.right = 22;
171
+ key.shadow.camera.top = 22;
172
+ key.shadow.camera.bottom = -22;
173
+ key.shadow.camera.near = 1;
174
+ key.shadow.camera.far = 70;
175
+ key.shadow.radius = 7;
176
+ key.shadow.bias = -.00035;
177
+ scene.add(key);
178
+ const fill = new THREE.DirectionalLight(0xffffff, 0);
179
+ fill.position.set(12, 10, 9);
180
+ scene.add(fill);
181
+
182
+ const floor = new THREE.Mesh(
183
+ new THREE.PlaneGeometry(160, 160),
184
+ new THREE.ShadowMaterial({ color: 0x3b3a35, opacity: .085 })
185
+ );
186
+ floor.rotation.x = -Math.PI / 2;
187
+ floor.position.y = -.67;
188
+ floor.receiveShadow = true;
189
+ scene.add(floor);
190
+
191
+ function randomFactory(seed) {
192
+ let value = seed >>> 0;
193
+ return () => {
194
+ value = (Math.imul(value, 1664525) + 1013904223) >>> 0;
195
+ return value / 4294967296;
196
+ };
197
+ }
198
+
199
+ function paperTexture(seed = 1, base = "#f2eee4", colored = false, wash = .22) {
200
+ const size = 256;
201
+ const cv = document.createElement("canvas");
202
+ cv.width = cv.height = size;
203
+ const g = cv.getContext("2d");
204
+ const random = randomFactory(seed);
205
+ g.fillStyle = base;
206
+ g.fillRect(0, 0, size, size);
207
+ const image = g.getImageData(0, 0, size, size);
208
+ const data = image.data;
209
+ for (let i = 0; i < data.length; i += 4) {
210
+ const grain = Math.round((random() - .5) * (colored ? 13 : 18));
211
+ data[i] = Math.max(0, Math.min(255, data[i] + grain));
212
+ data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + grain));
213
+ data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + grain));
214
+ }
215
+ g.putImageData(image, 0, 0);
216
+ if (colored) {
217
+ // Colored pencil never fully seals the paper: lift the pigment and leave tooth showing.
218
+ g.fillStyle = `rgba(243,239,229,${wash})`;
219
+ g.fillRect(0, 0, size, size);
220
+ g.lineCap = "round";
221
+ for (let pass = 0; pass < 2; pass++) {
222
+ g.beginPath();
223
+ for (let i = 0; i < 90; i++) {
224
+ const x = random() * size;
225
+ const y = random() * size;
226
+ const length = 8 + random() * 34;
227
+ const angle = (pass === 0 ? -.13 : .18) + (random() - .5) * .08;
228
+ g.moveTo(x, y);
229
+ g.lineTo(x + Math.cos(angle) * length, y + Math.sin(angle) * length);
230
+ }
231
+ g.strokeStyle = pass === 0 ? "rgba(34,35,31,.055)" : "rgba(255,247,230,.075)";
232
+ g.lineWidth = pass === 0 ? .8 : .65;
233
+ g.stroke();
234
+ }
235
+ g.beginPath();
236
+ for (let i = 0; i < 340; i++) {
237
+ const r = .35 + random() * 1.15;
238
+ g.rect(random() * size, random() * size, r, r * (.35 + random() * .5));
239
+ }
240
+ g.fillStyle = "rgba(246,241,229,.11)";
241
+ g.fill();
242
+ }
243
+ g.strokeStyle = colored ? "rgba(255,249,235,.11)" : "rgba(57,54,46,.04)";
244
+ g.lineWidth = colored ? .72 : .55;
245
+ g.beginPath();
246
+ for (let i = 0; i < 120; i++) {
247
+ const x = random() * size;
248
+ const y = random() * size;
249
+ g.moveTo(x, y);
250
+ g.lineTo(x + 5 + random() * 18, y + (random() - .5) * 2);
251
+ }
252
+ g.stroke();
253
+ const texture = new THREE.CanvasTexture(cv);
254
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
255
+ texture.repeat.set(3, 3);
256
+ texture.colorSpace = THREE.SRGBColorSpace;
257
+ texture.anisotropy = 8;
258
+ return texture;
259
+ }
260
+
261
+ function frostTexture(seed = 1) {
262
+ const size = 256;
263
+ const cv = document.createElement("canvas");
264
+ cv.width = cv.height = size;
265
+ const g = cv.getContext("2d");
266
+ const random = randomFactory(seed);
267
+ const image = g.createImageData(size, size);
268
+ for (let i = 0; i < image.data.length; i += 4) {
269
+ const broad = Math.sin(i / 740) * 5;
270
+ const grain = 204 + broad + (random() - .5) * 42;
271
+ image.data[i] = grain;
272
+ image.data[i + 1] = grain;
273
+ image.data[i + 2] = grain;
274
+ image.data[i + 3] = 255;
275
+ }
276
+ g.putImageData(image, 0, 0);
277
+ g.globalAlpha = .08;
278
+ g.fillStyle = "#ffffff";
279
+ for (let i = 0; i < 900; i++) {
280
+ const r = .25 + random() * .7;
281
+ g.fillRect(random() * size, random() * size, r, r);
282
+ }
283
+ const texture = new THREE.CanvasTexture(cv);
284
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
285
+ texture.repeat.set(4, 2.4);
286
+ texture.colorSpace = THREE.SRGBColorSpace;
287
+ texture.anisotropy = 8;
288
+ return texture;
289
+ }
290
+
291
+ function marbleTexture(seed = 1) {
292
+ const size = 512;
293
+ const cv = document.createElement("canvas");
294
+ cv.width = cv.height = size;
295
+ const g = cv.getContext("2d");
296
+ const random = randomFactory(seed);
297
+ g.fillStyle = "#fafaf8";
298
+ g.fillRect(0, 0, size, size);
299
+
300
+ // Sparse, low-contrast veins keep the base feeling white and architectural.
301
+ for (let i = 0; i < 8; i++) {
302
+ const startY = random() * size;
303
+ g.beginPath();
304
+ g.moveTo(-40, startY);
305
+ g.bezierCurveTo(
306
+ size * .22, startY - 90 + random() * 180,
307
+ size * .7, startY - 120 + random() * 240,
308
+ size + 40, startY - 70 + random() * 140
309
+ );
310
+ g.strokeStyle = i % 3 === 0 ? "rgba(154,164,166,.13)" : "rgba(190,184,177,.085)";
311
+ g.lineWidth = .45 + random() * 1.15;
312
+ g.stroke();
313
+ }
314
+ const texture = new THREE.CanvasTexture(cv);
315
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
316
+ texture.repeat.set(1.45, 1.1);
317
+ texture.colorSpace = THREE.SRGBColorSpace;
318
+ texture.anisotropy = 8;
319
+ return texture;
320
+ }
321
+
322
+ const paperMap = paperTexture(90210);
323
+ const frostMap = frostTexture(90213);
324
+ const marbleMap = marbleTexture(90214);
325
+ const codexMap = paperTexture(90211, "#343541", true, .34);
326
+ const claudeMap = paperTexture(90212, "#d97757", true, .2);
327
+ const topMaterial = new THREE.MeshLambertMaterial({ color: 0xf1ede2, map: paperMap, flatShading: true });
328
+ const sideMaterial = new THREE.MeshLambertMaterial({ color: 0xc9c5bb, map: paperMap, flatShading: true });
329
+ const codexMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, map: codexMap, flatShading: true });
330
+ const claudeMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, map: claudeMap, flatShading: true });
331
+ const codexTopMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, map: codexMap, side: THREE.DoubleSide, toneMapped: false, transparent: true, opacity: .64, depthWrite: false });
332
+ const claudeTopMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, map: claudeMap, side: THREE.DoubleSide, toneMapped: false, transparent: true, opacity: .64, depthWrite: false });
333
+ const chunkMaterial = new THREE.MeshLambertMaterial({ color: 0x595a57, map: paperMap, flatShading: true });
334
+ const inkMaterial = new THREE.LineBasicMaterial({ color: 0x30312d, transparent: true, opacity: .54 });
335
+ const faintInkMaterial = new THREE.LineBasicMaterial({ color: 0x30312d, transparent: true, opacity: .16 });
336
+
337
+ const hatchMaterials = [];
338
+ const dirtyPieces = [];
339
+ const dirtySlices = [];
340
+ const shadowMaterials = [];
341
+ const dirtyShellMaterials = [];
342
+ const glassMaterials = [];
343
+ const topLineMaterials = [];
344
+ const cleanGlassLineMaterials = [];
345
+ const platformDirtyMaterials = [];
346
+ const cleanPlatformMaterials = [];
347
+ const authenticGlassMaterials = [];
348
+ const authenticPlatformMaterials = [];
349
+ const repoLabels = [];
350
+
351
+ function outline(mesh, opacity = .56, threshold = 24) {
352
+ const geometry = new THREE.EdgesGeometry(mesh.geometry, threshold);
353
+ const main = new THREE.LineSegments(
354
+ geometry,
355
+ new THREE.LineBasicMaterial({ color: 0x2d2e2a, transparent: true, opacity })
356
+ );
357
+ main.renderOrder = 5;
358
+ mesh.add(main);
359
+ const echoGeometry = geometry.clone();
360
+ const echoPosition = echoGeometry.attributes.position;
361
+ const random = randomFactory(9000 + mesh.id * 17);
362
+ for (let i = 0; i < echoPosition.count; i++) {
363
+ echoPosition.setXYZ(
364
+ i,
365
+ echoPosition.getX(i) + (random() - .5) * .032,
366
+ echoPosition.getY(i) + (random() - .5) * .024,
367
+ echoPosition.getZ(i) + (random() - .5) * .032
368
+ );
369
+ }
370
+ const echo = new THREE.LineSegments(
371
+ echoGeometry,
372
+ new THREE.LineBasicMaterial({ color: 0x2d2e2a, transparent: true, opacity: .12 })
373
+ );
374
+ echo.position.set(.018, .012, -.014);
375
+ echo.renderOrder = 4;
376
+ mesh.add(echo);
377
+ return [main.material, echo.material];
378
+ }
379
+
380
+ const PLATFORM = { width: 27, depth: 20.5, thickness: .75, top: .22 };
381
+ const bodyPaperMaterial = topMaterial.clone();
382
+ bodyPaperMaterial.transparent = true;
383
+ bodyPaperMaterial.userData.dirtyOpacity = 1;
384
+ const body = new THREE.Mesh(
385
+ new RoundedBoxGeometry(PLATFORM.width, PLATFORM.thickness, PLATFORM.depth, 10, .82),
386
+ bodyPaperMaterial
387
+ );
388
+ body.position.y = PLATFORM.top - PLATFORM.thickness / 2;
389
+ body.castShadow = true;
390
+ body.receiveShadow = true;
391
+ scene.add(body);
392
+ platformDirtyMaterials.push(bodyPaperMaterial, ...outline(body, .42, 18));
393
+
394
+ for (const material of platformDirtyMaterials) {
395
+ if (material.userData.dirtyOpacity === undefined) material.userData.dirtyOpacity = material.opacity;
396
+ }
397
+
398
+ function makeMarbleMaterial() {
399
+ const material = new THREE.MeshPhysicalMaterial({
400
+ color: 0xffffff,
401
+ map: marbleMap,
402
+ roughness: .3,
403
+ metalness: 0,
404
+ clearcoat: .62,
405
+ clearcoatRoughness: .22,
406
+ transparent: true,
407
+ opacity: 0,
408
+ depthWrite: false
409
+ });
410
+ cleanPlatformMaterials.push(material);
411
+ return material;
412
+ }
413
+
414
+ const marbleBody = new THREE.Mesh(body.geometry.clone(), makeMarbleMaterial());
415
+ marbleBody.position.copy(body.position);
416
+ marbleBody.scale.set(1.002, 1.006, 1.002);
417
+ marbleBody.receiveShadow = true;
418
+ marbleBody.renderOrder = 2;
419
+ scene.add(marbleBody);
420
+
421
+ // Authentic setup-page tray, restored from main@07b9638. The historical city
422
+ // used one ivory clay slab with fine bump grain and a clipped grid decal.
423
+ const AUTHENTIC_TRAY = { width: 27, depth: 27, thickness: 1.35, top: .68 };
424
+ function authenticGrain() {
425
+ const size = 256;
426
+ const cv = document.createElement("canvas");
427
+ cv.width = cv.height = size;
428
+ const g = cv.getContext("2d");
429
+ const random = randomFactory(71621);
430
+ const image = g.createImageData(size, size);
431
+ for (let i = 0; i < image.data.length; i += 4) {
432
+ const n = 128 + (random() * 2 - 1) * 38;
433
+ image.data[i] = image.data[i + 1] = image.data[i + 2] = n;
434
+ image.data[i + 3] = 255;
435
+ }
436
+ g.putImageData(image, 0, 0);
437
+ const texture = new THREE.CanvasTexture(cv);
438
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
439
+ texture.anisotropy = 8;
440
+ texture.generateMipmaps = false;
441
+ texture.minFilter = THREE.LinearFilter;
442
+ texture.repeat.set(AUTHENTIC_TRAY.width / 3.8, AUTHENTIC_TRAY.depth / 3.8);
443
+ return texture;
444
+ }
445
+
446
+ const authenticTrayMaterial = new THREE.MeshPhysicalMaterial({
447
+ color: 0xece5d3,
448
+ roughness: .6,
449
+ metalness: 0,
450
+ clearcoat: .3,
451
+ clearcoatRoughness: .5,
452
+ envMapIntensity: .6,
453
+ bumpMap: authenticGrain(),
454
+ bumpScale: .022,
455
+ transparent: true,
456
+ opacity: 0,
457
+ depthWrite: false
458
+ });
459
+ authenticPlatformMaterials.push(authenticTrayMaterial);
460
+ const authenticTray = new THREE.Mesh(
461
+ new RoundedBoxGeometry(AUTHENTIC_TRAY.width, AUTHENTIC_TRAY.thickness, AUTHENTIC_TRAY.depth, 8, .65),
462
+ authenticTrayMaterial
463
+ );
464
+ authenticTray.position.y = AUTHENTIC_TRAY.top - AUTHENTIC_TRAY.thickness / 2;
465
+ authenticTray.castShadow = true;
466
+ authenticTray.receiveShadow = true;
467
+ authenticTray.renderOrder = 2;
468
+ scene.add(authenticTray);
469
+
470
+ function authenticGridTexture() {
471
+ const size = 768;
472
+ const cv = document.createElement("canvas");
473
+ cv.width = cv.height = size;
474
+ const g = cv.getContext("2d");
475
+ const margin = 32;
476
+ const radius = 58;
477
+ const span = size - margin * 2;
478
+ g.save();
479
+ g.beginPath();
480
+ g.roundRect(margin, margin, span, span, radius);
481
+ g.clip();
482
+ const cell = 54;
483
+ g.strokeStyle = "rgba(74,80,96,.45)";
484
+ g.lineWidth = 1.8;
485
+ g.beginPath();
486
+ for (let p = margin; p <= margin + span + .5; p += cell) {
487
+ g.moveTo(p, margin); g.lineTo(p, margin + span);
488
+ g.moveTo(margin, p); g.lineTo(margin + span, p);
489
+ }
490
+ g.stroke();
491
+ g.restore();
492
+ g.strokeStyle = "rgba(120,126,140,.24)";
493
+ g.lineWidth = 2.1;
494
+ g.beginPath();
495
+ g.roundRect(margin, margin, span, span, radius);
496
+ g.stroke();
497
+ const texture = new THREE.CanvasTexture(cv);
498
+ texture.colorSpace = THREE.SRGBColorSpace;
499
+ texture.anisotropy = 8;
500
+ return texture;
501
+ }
502
+
503
+ const authenticGridMaterial = new THREE.MeshBasicMaterial({
504
+ map: authenticGridTexture(),
505
+ transparent: true,
506
+ opacity: 0,
507
+ depthWrite: false,
508
+ toneMapped: false
509
+ });
510
+ authenticPlatformMaterials.push(authenticGridMaterial);
511
+ const authenticGrid = new THREE.Mesh(
512
+ new THREE.PlaneGeometry(AUTHENTIC_TRAY.width, AUTHENTIC_TRAY.depth),
513
+ authenticGridMaterial
514
+ );
515
+ authenticGrid.rotation.x = -Math.PI / 2;
516
+ authenticGrid.position.y = AUTHENTIC_TRAY.top + .012;
517
+ authenticGrid.renderOrder = 6;
518
+ scene.add(authenticGrid);
519
+
520
+ function addPlatformGrid() {
521
+ const vertices = [];
522
+ const y = PLATFORM.top + .028;
523
+ const insetX = PLATFORM.width / 2 - 1.05;
524
+ const insetZ = PLATFORM.depth / 2 - 1.05;
525
+ const step = 1.7;
526
+ for (let x = -insetX; x <= insetX; x += step) vertices.push(x, y, -insetZ, x, y, insetZ);
527
+ for (let z = -insetZ; z <= insetZ; z += step) vertices.push(-insetX, y, z, insetX, y, z);
528
+ const geometry = new THREE.BufferGeometry();
529
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
530
+ const material = new THREE.LineBasicMaterial({ color: 0x50514c, transparent: true, opacity: .105 });
531
+ material.userData.dirtyOpacity = .105;
532
+ scene.add(new THREE.LineSegments(geometry, material));
533
+ platformDirtyMaterials.push(material);
534
+ }
535
+
536
+ function addPlatformSideGrain() {
537
+ const random = randomFactory(5519);
538
+ const vertices = [];
539
+ const frontZ = PLATFORM.depth / 2 + .004;
540
+ const bottom = -PLATFORM.thickness + .14;
541
+ const top = -.05;
542
+ for (let x = -PLATFORM.width / 2 + .9; x < PLATFORM.width / 2 - .9; x += .52) {
543
+ if (random() < .2) continue;
544
+ const jitter = (random() - .5) * .1;
545
+ vertices.push(x + jitter, bottom + random() * .18, frontZ, x + jitter + (random() - .5) * .12, top - random() * .22, frontZ);
546
+ }
547
+ const rightX = PLATFORM.width / 2 + .004;
548
+ for (let z = -PLATFORM.depth / 2 + .9; z < PLATFORM.depth / 2 - .9; z += .55) {
549
+ if (random() < .24) continue;
550
+ const jitter = (random() - .5) * .1;
551
+ vertices.push(rightX, bottom + random() * .2, z + jitter, rightX, top - random() * .2, z + jitter + (random() - .5) * .12);
552
+ }
553
+ // Sparse diagonal passes keep the wall hand-shaded without turning it into a striped field.
554
+ for (let x = -PLATFORM.width / 2 + 1.2; x < PLATFORM.width / 2 - 1.2; x += 2.3) {
555
+ const startY = bottom + .18 + random() * .45;
556
+ vertices.push(
557
+ x, startY, frontZ + .002,
558
+ Math.min(PLATFORM.width / 2 - .7, x + .8 + random() * .5),
559
+ Math.min(top - .08, startY + .35),
560
+ frontZ + .002
561
+ );
562
+ }
563
+ const geometry = new THREE.BufferGeometry();
564
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
565
+ const material = new THREE.LineBasicMaterial({ color: 0x373833, transparent: true, opacity: .19 });
566
+ material.userData.dirtyOpacity = .19;
567
+ scene.add(new THREE.LineSegments(geometry, material));
568
+ platformDirtyMaterials.push(material);
569
+ }
570
+
571
+ addPlatformGrid();
572
+ addPlatformSideGrain();
573
+
574
+ const repos = [
575
+ { name: "WebPageReactVersion", hours: 92.8, x: -5.3, z: -2.1, w: 4.2, d: 4.0, h: 8.5, slices: 5, codexPct: 31, claudePct: 69, label: true },
576
+ { name: "EchoMem-Chrome", hours: 70.1, x: 2.2, z: -2.5, w: 3.9, d: 3.7, h: 7.2, slices: 5, codexPct: 19, claudePct: 81, label: true },
577
+ { name: "echo-design-system", hours: 21.2, x: -1.2, z: 3.6, w: 2.9, d: 2.7, h: 4.4, slices: 4, codexPct: 44, claudePct: 56 },
578
+ { name: "Kobe Life", hours: 9.6, x: 6.8, z: 3.7, w: 2.1, d: 2.0, h: 3.0, slices: 3, codexPct: 100, claudePct: 0 },
579
+ { name: "public", hours: 1.9, x: -7.9, z: 4.8, w: 1.7, d: 1.6, h: 2.1, slices: 2, codexPct: 0, claudePct: 100 },
580
+ { name: "EchoMemory_log", hours: 1.8, x: 3.0, z: 4.4, w: 1.65, d: 1.6, h: 1.95, slices: 2, codexPct: 63, claudePct: 37 },
581
+ { name: "brands", hours: .2, x: 8.5, z: -.5, w: 1.25, d: 1.2, h: 1.45, slices: 1, codexPct: 0, claudePct: 100 }
582
+ ];
583
+
584
+ function makeGraphiteShadow(repo, seed) {
585
+ const size = 256;
586
+ const cv = document.createElement("canvas");
587
+ cv.width = cv.height = size;
588
+ const g = cv.getContext("2d");
589
+ const random = randomFactory(seed);
590
+ const cx = 118;
591
+ const cy = 136;
592
+ const rx = 82;
593
+ const ry = 48;
594
+
595
+ g.save();
596
+ g.filter = "blur(7px)";
597
+ g.fillStyle = "rgba(35,36,32,.10)";
598
+ g.beginPath();
599
+ g.ellipse(cx + 12, cy + 8, rx, ry, -.18, 0, Math.PI * 2);
600
+ g.fill();
601
+ g.restore();
602
+
603
+ for (let pass = 0; pass < 2; pass++) {
604
+ g.beginPath();
605
+ const count = 190;
606
+ for (let i = 0; i < count; i++) {
607
+ const ring = Math.pow(random(), .68);
608
+ const a = random() * Math.PI * 2;
609
+ const x = cx + Math.cos(a) * rx * ring + random() * 28;
610
+ const y = cy + Math.sin(a) * ry * ring + random() * 14;
611
+ const length = 3 + random() * 13;
612
+ const angle = pass === 0 ? -.42 : .38;
613
+ if (random() < .18) continue;
614
+ g.moveTo(x, y);
615
+ g.lineTo(x + Math.cos(angle) * length, y + Math.sin(angle) * length * .7);
616
+ }
617
+ g.strokeStyle = pass === 0 ? "rgba(38,39,35,.16)" : "rgba(38,39,35,.10)";
618
+ g.lineWidth = .75;
619
+ g.lineCap = "round";
620
+ g.stroke();
621
+ }
622
+
623
+ g.beginPath();
624
+ for (let i = 0; i < 190; i++) {
625
+ const a = random() * Math.PI * 2;
626
+ const ring = Math.pow(random(), .7);
627
+ g.rect(cx + Math.cos(a) * rx * ring, cy + Math.sin(a) * ry * ring, .5 + random() * 1.3, .5 + random());
628
+ }
629
+ g.fillStyle = "rgba(45,45,41,.08)";
630
+ g.fill();
631
+
632
+ const texture = new THREE.CanvasTexture(cv);
633
+ texture.colorSpace = THREE.SRGBColorSpace;
634
+ const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true, depthWrite: false, opacity: 1, toneMapped: false });
635
+ const plane = new THREE.Mesh(new THREE.PlaneGeometry(repo.w * 1.65, repo.d * 1.25), material);
636
+ plane.rotation.x = -Math.PI / 2;
637
+ plane.rotation.z = -.18;
638
+ plane.position.set(repo.x + repo.w * .18, PLATFORM.top + .048, repo.z + repo.d * .12);
639
+ plane.renderOrder = 2;
640
+ scene.add(plane);
641
+ shadowMaterials.push(material);
642
+ }
643
+
644
+ function faceHatching(mesh, w, h, d, intensity, seed) {
645
+ const random = randomFactory(seed);
646
+ const vertices = [];
647
+ const count = Math.max(5, Math.round((w + h) * intensity * .82));
648
+ const front = d / 2 + .015;
649
+ for (let i = 0; i < count; i++) {
650
+ if (random() < .17) continue;
651
+ const y = -h / 2 + .2 + random() * Math.max(.1, h - .4);
652
+ const x = -w / 2 + .18 + random() * Math.max(.1, w - .36);
653
+ const length = .35 + random() * Math.min(1.5, w * .52);
654
+ const rise = length * (.28 + random() * .18);
655
+ const endX = Math.min(w / 2 - .1, x + length);
656
+ vertices.push(x, y, front, endX, Math.min(h / 2 - .1, y + rise), front);
657
+ }
658
+ const right = w / 2 + .015;
659
+ for (let i = 0; i < Math.round(count * .58); i++) {
660
+ if (random() < .22) continue;
661
+ const y = -h / 2 + .2 + random() * Math.max(.1, h - .4);
662
+ const z = -d / 2 + .15 + random() * Math.max(.1, d - .3);
663
+ const length = .3 + random() * Math.min(1.1, d * .45);
664
+ vertices.push(right, y, z, right, Math.min(h / 2 - .1, y + length * .34), Math.min(d / 2 - .08, z + length));
665
+ }
666
+ const geometry = new THREE.BufferGeometry();
667
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
668
+ const material = new THREE.LineBasicMaterial({ color: 0x30312d, transparent: true, opacity: .32 });
669
+ material.userData.baseOpacity = .32;
670
+ const lines = new THREE.LineSegments(geometry, material);
671
+ lines.renderOrder = 6;
672
+ mesh.add(lines);
673
+ hatchMaterials.push(material);
674
+ }
675
+
676
+ function crackMarks(mesh, w, h, d, seed) {
677
+ const random = randomFactory(seed);
678
+ const vertices = [];
679
+ const front = d / 2 + .02;
680
+ const count = Math.max(1, Math.round(w * .65));
681
+ for (let i = 0; i < count; i++) {
682
+ const x = (random() - .5) * w * .72;
683
+ const y = (random() - .5) * h * .52;
684
+ vertices.push(x, y, front, x + (random() - .5) * .42, y - .25 - random() * .35, front);
685
+ if (random() > .45) vertices.push(x, y - .22, front, x + (random() - .5) * .4, y - .45 - random() * .32, front);
686
+ }
687
+ const geometry = new THREE.BufferGeometry();
688
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
689
+ const material = new THREE.LineBasicMaterial({ color: 0x292a27, transparent: true, opacity: .46 });
690
+ material.userData.baseOpacity = .46;
691
+ mesh.add(new THREE.LineSegments(geometry, material));
692
+ hatchMaterials.push(material);
693
+ }
694
+
695
+ function labelTexture(repo) {
696
+ const cv = document.createElement("canvas");
697
+ cv.width = 512;
698
+ cv.height = 180;
699
+ const g = cv.getContext("2d");
700
+ g.textAlign = "center";
701
+ g.fillStyle = "#343530";
702
+ g.font = "600 62px IBM Plex Mono, monospace";
703
+ g.fillText(`${repo.hours}h`, 256, 68);
704
+ g.fillStyle = "rgba(52,53,48,.72)";
705
+ g.font = "500 23px IBM Plex Mono, monospace";
706
+ g.fillText(repo.name, 256, 112);
707
+ const texture = new THREE.CanvasTexture(cv);
708
+ texture.colorSpace = THREE.SRGBColorSpace;
709
+ return texture;
710
+ }
711
+
712
+ // Verbatim optical model from the setup-page city on main@07b9638.
713
+ function authenticClayGlass(hex) {
714
+ const color = new THREE.Color(hex);
715
+ const material = new THREE.MeshPhysicalMaterial({
716
+ color,
717
+ roughness: .42,
718
+ metalness: 0,
719
+ transmission: .85,
720
+ ior: 1.34,
721
+ thickness: 70,
722
+ attenuationColor: color.clone(),
723
+ attenuationDistance: 95,
724
+ clearcoat: .28,
725
+ clearcoatRoughness: .45,
726
+ envMapIntensity: 1.05,
727
+ transparent: true,
728
+ opacity: 0,
729
+ depthWrite: false
730
+ });
731
+ authenticGlassMaterials.push(material);
732
+ return material;
733
+ }
734
+
735
+ // The historical implementation only rounded the exterior top/bottom of a
736
+ // split tower, leaving the provider seam perfectly planar.
737
+ function selectiveRoundedBoxGeometry(width, height, depth, segments = 8, radius = .1, opts = {}) {
738
+ const roundTop = opts.roundTop !== false;
739
+ const roundBottom = opts.roundBottom !== false;
740
+ const subdivisions = segments * 2 + 1;
741
+ const r = Math.min(width / 2, height / 2, depth / 2, radius);
742
+ const base = new THREE.BoxGeometry(1, 1, 1, subdivisions, subdivisions, subdivisions);
743
+ const geometry = base.toNonIndexed();
744
+ base.dispose();
745
+ const positions = geometry.attributes.position.array;
746
+ const normals = geometry.attributes.normal.array;
747
+ const position = new THREE.Vector3();
748
+ const normal = new THREE.Vector3();
749
+ const faceSpan = positions.length / 6;
750
+ const halfStep = .5 / subdivisions;
751
+ const bx = width / 2 - r;
752
+ const by = height / 2 - r;
753
+ const bz = depth / 2 - r;
754
+ for (let i = 0; i < positions.length; i += 3) {
755
+ position.fromArray(positions, i);
756
+ const sx = Math.sign(position.x) || 1;
757
+ const sy = Math.sign(position.y) || 1;
758
+ const sz = Math.sign(position.z) || 1;
759
+ const side = Math.floor(i / faceSpan);
760
+ const roundY = sy > 0 ? roundTop : roundBottom;
761
+ normal.copy(position);
762
+ normal.x -= sx * halfStep;
763
+ normal.z -= sz * halfStep;
764
+ if (roundY) normal.y -= sy * halfStep;
765
+ else normal.y = 0;
766
+ if (normal.lengthSq() > 0) normal.normalize();
767
+ positions[i] = bx * sx + normal.x * r;
768
+ positions[i + 1] = roundY ? by * sy + normal.y * r : height / 2 * sy;
769
+ positions[i + 2] = bz * sz + normal.z * r;
770
+ if (!roundY && (side === 2 || side === 3)) {
771
+ normals[i] = 0; normals[i + 1] = sy; normals[i + 2] = 0;
772
+ } else {
773
+ normals[i] = normal.x; normals[i + 1] = normal.y; normals[i + 2] = normal.z;
774
+ }
775
+ }
776
+ geometry.attributes.position.needsUpdate = true;
777
+ geometry.attributes.normal.needsUpdate = true;
778
+ geometry.computeBoundingBox();
779
+ geometry.computeBoundingSphere();
780
+ return geometry;
781
+ }
782
+
783
+ function createRepo(repo, index) {
784
+ const authenticSize = { width: repo.w, height: repo.h, depth: repo.d };
785
+ const scale = Math.sqrt(Math.max(.001, repo.hours) / 92.8);
786
+ const radius = .76 + scale * 2.52;
787
+ const thickness = .44 + scale * 1.08;
788
+ repo.w = radius * 2;
789
+ repo.d = radius * 2;
790
+ repo.h = thickness;
791
+ makeGraphiteShadow(repo, 800 + index * 31);
792
+
793
+ const group = new THREE.Group();
794
+ group.position.set(repo.x, PLATFORM.top, repo.z);
795
+ scene.add(group);
796
+
797
+ const random = randomFactory(3000 + index * 101);
798
+ const puck = new THREE.Group();
799
+ const shiftX = (random() - .5) * Math.min(.26, radius * .1);
800
+ const shiftZ = (random() - .5) * Math.min(.22, radius * .08);
801
+ const tiltX = (random() - .5) * .035;
802
+ const tiltZ = (random() - .5) * .035;
803
+ const turn = (random() - .5) * .045;
804
+ puck.position.set(shiftX, thickness / 2, shiftZ);
805
+ puck.rotation.set(tiltX, turn, tiltZ);
806
+ group.add(puck);
807
+
808
+ const wall = sideMaterial.clone();
809
+ wall.color.setScalar(.94 + random() * .035);
810
+ wall.transparent = true;
811
+ wall.opacity = 1;
812
+ const topPaper = topMaterial.clone();
813
+ topPaper.transparent = true;
814
+ topPaper.opacity = 1;
815
+ dirtyShellMaterials.push(wall, topPaper);
816
+ const cylinderGeometry = new THREE.CylinderGeometry(radius, radius, thickness, 96, 1, false);
817
+ const cylinder = new THREE.Mesh(
818
+ cylinderGeometry,
819
+ [wall, topPaper, wall]
820
+ );
821
+ cylinder.castShadow = true;
822
+ cylinder.receiveShadow = true;
823
+ puck.add(cylinder);
824
+
825
+ const start = -Math.PI / 2;
826
+ const claudeAngle = Math.PI * 2 * repo.claudePct / 100;
827
+ const codexAngle = Math.PI * 2 * repo.codexPct / 100;
828
+ const codexGlass = new THREE.Color(0x7fb7d8);
829
+ const claudeGlass = new THREE.Color(0xda8da4);
830
+
831
+ // Clear context becomes an ordered glass city, not a stretched circular puck.
832
+ // Provider share is preserved as adjacent translucent blocks inside one footprint.
833
+ const cleanWidth = Math.max(.76, radius * .88);
834
+ const cleanDepth = Math.max(.68, radius * .72);
835
+ const cleanHeight = Math.max(1.75, thickness * 4.65);
836
+ const cleanCity = new THREE.Group();
837
+ cleanCity.scale.y = .06;
838
+ cleanCity.visible = false;
839
+ group.add(cleanCity);
840
+
841
+ const authenticCity = new THREE.Group();
842
+ authenticCity.visible = false;
843
+ group.add(authenticCity);
844
+ const authenticRadius = Math.min(
845
+ Math.min(authenticSize.width, authenticSize.height, authenticSize.depth) * .12,
846
+ .65
847
+ );
848
+ const authenticSegments = [];
849
+ if (repo.claudePct > 0) authenticSegments.push({
850
+ height: authenticSize.height * repo.claudePct / 100,
851
+ color: 0xd97757
852
+ });
853
+ if (repo.codexPct > 0) authenticSegments.push({
854
+ height: authenticSize.height * repo.codexPct / 100,
855
+ color: 0x10a37f
856
+ });
857
+ let authenticY = AUTHENTIC_TRAY.top - PLATFORM.top;
858
+ authenticSegments.forEach((segment, segmentIndex) => {
859
+ const mixed = authenticSegments.length > 1;
860
+ const geometry = mixed
861
+ ? selectiveRoundedBoxGeometry(
862
+ authenticSize.width,
863
+ segment.height,
864
+ authenticSize.depth,
865
+ 8,
866
+ Math.min(authenticRadius, segment.height * .45),
867
+ { roundBottom: segmentIndex === 0, roundTop: segmentIndex === authenticSegments.length - 1 }
868
+ )
869
+ : new RoundedBoxGeometry(
870
+ authenticSize.width,
871
+ segment.height,
872
+ authenticSize.depth,
873
+ 8,
874
+ Math.min(authenticRadius, segment.height * .45)
875
+ );
876
+ const tower = new THREE.Mesh(geometry, authenticClayGlass(segment.color));
877
+ tower.position.y = authenticY + segment.height / 2;
878
+ tower.castShadow = true;
879
+ tower.receiveShadow = true;
880
+ tower.renderOrder = 4;
881
+ authenticCity.add(tower);
882
+ authenticY += segment.height;
883
+ });
884
+
885
+ function addGlassBlock(share, color, cursor) {
886
+ if (share <= .0001) return cursor;
887
+ const gap = repo.codexPct > 0 && repo.claudePct > 0 ? .055 : 0;
888
+ const availableWidth = cleanWidth - gap;
889
+ const width = Math.max(.12, availableWidth * share);
890
+ const centerX = -cleanWidth / 2 + cursor + width / 2;
891
+ const surfaceColor = color.clone();
892
+ const material = new THREE.MeshPhysicalMaterial({
893
+ color: surfaceColor,
894
+ roughness: .42,
895
+ roughnessMap: frostMap,
896
+ bumpMap: frostMap,
897
+ bumpScale: .0018,
898
+ metalness: 0,
899
+ transmission: .85,
900
+ thickness: 2.4,
901
+ ior: 1.34,
902
+ attenuationColor: color,
903
+ attenuationDistance: 3.25,
904
+ clearcoat: .28,
905
+ clearcoatRoughness: .45,
906
+ envMapIntensity: 1.05,
907
+ transparent: true,
908
+ opacity: 0,
909
+ depthWrite: false,
910
+ side: THREE.DoubleSide
911
+ });
912
+ material.userData.cleanOpacity = .96;
913
+ const geometry = new RoundedBoxGeometry(width, cleanHeight, cleanDepth, 5, Math.min(.055, width * .08));
914
+ const block = new THREE.Mesh(geometry, material);
915
+ block.position.set(centerX, cleanHeight / 2, 0);
916
+ // Three.js shadow maps treat transmissive glass as opaque and create giant
917
+ // charcoal rectangles, so the clean glass uses studio light without a hard cast shadow.
918
+ block.castShadow = false;
919
+ block.receiveShadow = true;
920
+ block.renderOrder = 3;
921
+ cleanCity.add(block);
922
+ glassMaterials.push(material);
923
+
924
+ const edgeMaterial = new THREE.LineBasicMaterial({
925
+ color: color.clone().lerp(new THREE.Color(0xffffff), .62),
926
+ transparent: true,
927
+ opacity: 0,
928
+ depthWrite: false
929
+ });
930
+ const edges = new THREE.LineSegments(new THREE.EdgesGeometry(geometry, 24), edgeMaterial);
931
+ edges.renderOrder = 7;
932
+ block.add(edges);
933
+ cleanGlassLineMaterials.push({ material: edgeMaterial, clean: .48 });
934
+
935
+ return cursor + width + gap;
936
+ }
937
+
938
+ let glassCursor = 0;
939
+ glassCursor = addGlassBlock(repo.claudePct / 100, claudeGlass, glassCursor);
940
+ addGlassBlock(repo.codexPct / 100, codexGlass, glassCursor);
941
+
942
+ const topY = thickness / 2 + .001;
943
+ const insetRadius = radius * .965;
944
+
945
+ function addCleanGlassArc(angle, length, color, y, cleanOpacity) {
946
+ if (length <= .0001) return;
947
+ const points = [];
948
+ const segments = Math.max(12, Math.round(96 * length / (Math.PI * 2)));
949
+ for (let i = 0; i <= segments; i++) {
950
+ const a = angle + length * i / segments;
951
+ points.push(new THREE.Vector3(Math.cos(a) * radius * 1.006, y, Math.sin(a) * radius * 1.006));
952
+ }
953
+ const material = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0, depthWrite: false });
954
+ const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), material);
955
+ line.renderOrder = 7;
956
+ puck.add(line);
957
+ cleanGlassLineMaterials.push({ material, clean: cleanOpacity });
958
+ }
959
+
960
+ addCleanGlassArc(start, claudeAngle, claudeGlass, topY + .0012, .68);
961
+ addCleanGlassArc(start + claudeAngle, codexAngle, codexGlass, topY + .0012, .68);
962
+ addCleanGlassArc(start, claudeAngle, claudeGlass, -thickness / 2 + .012, .055);
963
+ addCleanGlassArc(start + claudeAngle, codexAngle, codexGlass, -thickness / 2 + .012, .055);
964
+
965
+ for (const [angle, color] of [[start, claudeGlass], [start + claudeAngle, codexGlass]]) {
966
+ const x = Math.cos(angle) * radius * 1.006;
967
+ const z = Math.sin(angle) * radius * 1.006;
968
+ const material = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0, depthWrite: false });
969
+ const geometry = new THREE.BufferGeometry().setFromPoints([
970
+ new THREE.Vector3(x, -thickness / 2 + .02, z),
971
+ new THREE.Vector3(x, thickness / 2 - .01, z)
972
+ ]);
973
+ const line = new THREE.Line(geometry, material);
974
+ line.renderOrder = 7;
975
+ puck.add(line);
976
+ cleanGlassLineMaterials.push({ material, clean: .52 });
977
+ }
978
+ if (claudeAngle > .0001) {
979
+ const geometry = new THREE.CircleGeometry(insetRadius, 72, start, claudeAngle);
980
+ geometry.rotateX(-Math.PI / 2);
981
+ const sector = new THREE.Mesh(geometry, claudeTopMaterial);
982
+ sector.position.y = topY;
983
+ sector.renderOrder = 4;
984
+ puck.add(sector);
985
+ }
986
+ if (codexAngle > .0001) {
987
+ const geometry = new THREE.CircleGeometry(insetRadius, 72, start + claudeAngle, codexAngle);
988
+ geometry.rotateX(-Math.PI / 2);
989
+ const sector = new THREE.Mesh(geometry, codexTopMaterial);
990
+ sector.position.y = topY + .0002;
991
+ sector.renderOrder = 4;
992
+ puck.add(sector);
993
+ }
994
+
995
+ // Pencil boundaries: doubled circumference plus the two radial split seams.
996
+ const circlePoints = [];
997
+ for (let i = 0; i <= 96; i++) {
998
+ const angle = i / 96 * Math.PI * 2;
999
+ circlePoints.push(new THREE.Vector3(Math.cos(angle) * radius, topY + .0005, Math.sin(angle) * radius));
1000
+ }
1001
+ const rimMaterial = new THREE.LineBasicMaterial({ color: 0x2d2e2a, transparent: true, opacity: .72 });
1002
+ const rim = new THREE.Line(
1003
+ new THREE.BufferGeometry().setFromPoints(circlePoints),
1004
+ rimMaterial
1005
+ );
1006
+ puck.add(rim);
1007
+ topLineMaterials.push({ material: rimMaterial, dirty: .72, clean: .035 });
1008
+ const rimEcho = rim.clone();
1009
+ const rimEchoMaterial = new THREE.LineBasicMaterial({ color: 0x2d2e2a, transparent: true, opacity: .16 });
1010
+ rimEcho.material = rimEchoMaterial;
1011
+ rimEcho.scale.set(1.006, 1, .996);
1012
+ puck.add(rimEcho);
1013
+ topLineMaterials.push({ material: rimEchoMaterial, dirty: .16, clean: .008 });
1014
+
1015
+ const seamVertices = [];
1016
+ for (const angle of [start, start + claudeAngle]) {
1017
+ seamVertices.push(0, topY + .0007, 0, Math.cos(angle) * insetRadius, topY + .0007, Math.sin(angle) * insetRadius);
1018
+ }
1019
+ const seamGeometry = new THREE.BufferGeometry();
1020
+ seamGeometry.setAttribute("position", new THREE.Float32BufferAttribute(seamVertices, 3));
1021
+ const seamMaterial = new THREE.LineBasicMaterial({ color: 0x292a27, transparent: true, opacity: .62 });
1022
+ puck.add(new THREE.LineSegments(seamGeometry, seamMaterial));
1023
+ topLineMaterials.push({ material: seamMaterial, dirty: .62, clean: .025 });
1024
+
1025
+ // Cylindrical wall grain: mostly vertical, with a few soft cross-hatched strokes on the dark side.
1026
+ const sideVertices = [];
1027
+ const strokeCount = Math.max(28, Math.round(radius * 19));
1028
+ for (let i = 0; i < strokeCount; i++) {
1029
+ const angle = random() * Math.PI * 2;
1030
+ if (random() < .16) continue;
1031
+ const x = Math.cos(angle) * (radius + .009);
1032
+ const z = Math.sin(angle) * (radius + .009);
1033
+ const bottom = -thickness / 2 + .08 + random() * .12;
1034
+ const top = thickness / 2 - .08 - random() * .12;
1035
+ sideVertices.push(x, bottom, z, x + (random() - .5) * .025, top, z + (random() - .5) * .025);
1036
+ if (Math.sin(angle + .8) > .15 && random() > .58) {
1037
+ const rise = .12 + random() * thickness * .24;
1038
+ const sweep = .055 + random() * .09;
1039
+ sideVertices.push(
1040
+ x, bottom + random() * Math.max(.1, thickness - .3), z,
1041
+ Math.cos(angle + sweep) * (radius + .011), Math.min(top, bottom + rise + random() * .35), Math.sin(angle + sweep) * (radius + .011)
1042
+ );
1043
+ }
1044
+ }
1045
+ const sideGeometry = new THREE.BufferGeometry();
1046
+ sideGeometry.setAttribute("position", new THREE.Float32BufferAttribute(sideVertices, 3));
1047
+ const sideInk = new THREE.LineBasicMaterial({ color: 0x30312d, transparent: true, opacity: .22 });
1048
+ sideInk.userData.baseOpacity = .22;
1049
+ puck.add(new THREE.LineSegments(sideGeometry, sideInk));
1050
+ hatchMaterials.push(sideInk);
1051
+
1052
+ // Bottom seam carries more graphite pressure, like the reference drawing.
1053
+ const bottomPoints = circlePoints.map(point => new THREE.Vector3(point.x, -thickness / 2 + .001, point.z));
1054
+ const bottomRimMaterial = new THREE.LineBasicMaterial({ color: 0x282924, transparent: true, opacity: .5 });
1055
+ bottomRimMaterial.userData.baseOpacity = .5;
1056
+ const bottomRim = new THREE.Line(
1057
+ new THREE.BufferGeometry().setFromPoints(bottomPoints),
1058
+ bottomRimMaterial
1059
+ );
1060
+ puck.add(bottomRim);
1061
+ hatchMaterials.push(bottomRimMaterial);
1062
+
1063
+ dirtySlices.push({
1064
+ mesh: puck,
1065
+ dirtyPosition: puck.position.clone(),
1066
+ dirtyRotation: turn,
1067
+ dirtyRotationX: tiltX,
1068
+ dirtyRotationZ: tiltZ,
1069
+ baseThickness: thickness,
1070
+ cleanCity,
1071
+ cleanHeight,
1072
+ authenticCity,
1073
+ authenticHeight: authenticSize.height,
1074
+ dirtyCylinder: cylinder,
1075
+ });
1076
+
1077
+ if (repo.label) {
1078
+ const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: labelTexture(repo), transparent: true, depthWrite: false, toneMapped: false }));
1079
+ sprite.scale.set(radius * 2.05, radius * .73, 1);
1080
+ sprite.position.set(repo.x, PLATFORM.top + thickness + 1.0, repo.z);
1081
+ scene.add(sprite);
1082
+ repoLabels.push({
1083
+ sprite,
1084
+ dirtyY: sprite.position.y,
1085
+ cleanY: PLATFORM.top + cleanHeight + 1.1,
1086
+ authenticY: AUTHENTIC_TRAY.top + authenticSize.height + 1.1
1087
+ });
1088
+ }
1089
+
1090
+ const pieceCount = thickness > 1.2 ? 2 : thickness > .8 ? 1 : 0;
1091
+ for (let i = 0; i < pieceCount; i++) {
1092
+ const size = .22 + random() * .28;
1093
+ const piece = new THREE.Mesh(
1094
+ i % 2 ? new THREE.TetrahedronGeometry(size, 0) : new THREE.BoxGeometry(size, size * .65, size * .8),
1095
+ chunkMaterial
1096
+ );
1097
+ const side = random() > .5 ? 1 : -1;
1098
+ piece.position.set(repo.x + side * (radius * 1.08 + random() * .42), PLATFORM.top + size * .5, repo.z + (random() - .5) * radius * 1.3);
1099
+ piece.rotation.set(random() * 1.2, random() * 1.2, random() * 1.2);
1100
+ piece.castShadow = true;
1101
+ scene.add(piece);
1102
+ outline(piece, .42, 15);
1103
+ dirtyPieces.push(piece);
1104
+ }
1105
+ }
1106
+
1107
+ repos.forEach(createRepo);
1108
+
1109
+ function addDirtyTrail() {
1110
+ const random = randomFactory(7711);
1111
+ const vertices = [];
1112
+ for (let i = 0; i < 24; i++) {
1113
+ const x = -9.6 + random() * 18.5;
1114
+ const z = -6.7 + random() * 13.3;
1115
+ if (random() < .6) continue;
1116
+ const length = .22 + random() * .72;
1117
+ vertices.push(x, PLATFORM.top + .055, z, x + length, PLATFORM.top + .057, z + length * (.3 + random() * .22));
1118
+ }
1119
+ const geometry = new THREE.BufferGeometry();
1120
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
1121
+ const material = new THREE.LineBasicMaterial({ color: 0x30312d, transparent: true, opacity: .2 });
1122
+ material.userData.baseOpacity = .2;
1123
+ scene.add(new THREE.LineSegments(geometry, material));
1124
+ hatchMaterials.push(material);
1125
+ }
1126
+ addDirtyTrail();
1127
+
1128
+ const dirtyButton = document.getElementById("dirty");
1129
+ const repairButton = document.getElementById("repair");
1130
+ const authenticButton = document.getElementById("authentic");
1131
+ const dirtyValue = document.getElementById("dirty-value");
1132
+ const dirtyLabel = document.getElementById("dirty-label");
1133
+ const summaryCaption = document.getElementById("summary-caption");
1134
+ let repairTarget = 0;
1135
+ let repairProgress = 0;
1136
+
1137
+ function setVersion(version) {
1138
+ repairTarget = version === "dirty" ? 0 : 1;
1139
+ dirtyButton.setAttribute("aria-pressed", String(version === "dirty"));
1140
+ repairButton.setAttribute("aria-pressed", String(version === "repair"));
1141
+ authenticButton.setAttribute("aria-pressed", String(version === "authentic"));
1142
+ dirtyValue.textContent = version === "dirty" ? "82%" : "5%";
1143
+ dirtyLabel.textContent = version === "dirty" ? "dirty context" : "residual context";
1144
+ summaryCaption.textContent = version === "dirty"
1145
+ ? "Dirty context spreads low and wide, carrying graphite, dust and repeated reads."
1146
+ : version === "repair"
1147
+ ? "Repair city: authentic frosted volume glass on one ivory clay tray."
1148
+ : "Authentic setup-page city: frosted volume glass on one ivory clay tray.";
1149
+ }
1150
+
1151
+ dirtyButton.addEventListener("click", () => setVersion("dirty"));
1152
+ repairButton.addEventListener("click", () => setVersion("repair"));
1153
+ authenticButton.addEventListener("click", () => setVersion("authentic"));
1154
+
1155
+ function updateRepair() {
1156
+ repairProgress += (repairTarget - repairProgress) * .075;
1157
+ const ease = repairProgress * repairProgress * (3 - 2 * repairProgress);
1158
+ for (const material of dirtyShellMaterials) {
1159
+ material.opacity = THREE.MathUtils.lerp(1, .018, ease);
1160
+ material.depthWrite = ease < .52;
1161
+ }
1162
+ // Repair now uses the exact same geometry, optics, palette and base as the
1163
+ // authentic setup-page city. The old marble/side-by-side experiment remains
1164
+ // allocated only so the dirty morphology code stays easy to compare, but it
1165
+ // never enters the render pass and therefore cannot corrupt transparent sort.
1166
+ for (const material of glassMaterials) material.opacity = 0;
1167
+ for (const material of authenticGlassMaterials) {
1168
+ material.opacity = ease;
1169
+ material.depthWrite = ease > .97;
1170
+ }
1171
+ for (const entry of topLineMaterials) {
1172
+ entry.material.opacity = THREE.MathUtils.lerp(entry.dirty, 0, ease);
1173
+ }
1174
+ for (const entry of cleanGlassLineMaterials) entry.material.opacity = 0;
1175
+ for (const material of platformDirtyMaterials) {
1176
+ material.opacity = material.userData.dirtyOpacity * (1 - ease);
1177
+ material.depthWrite = ease < .5;
1178
+ }
1179
+ for (const material of cleanPlatformMaterials) material.opacity = 0;
1180
+ for (const material of authenticPlatformMaterials) {
1181
+ material.opacity = ease;
1182
+ if (material === authenticTrayMaterial) material.depthWrite = ease > .02;
1183
+ }
1184
+ codexTopMaterial.opacity = THREE.MathUtils.lerp(.64, 0, ease);
1185
+ claudeTopMaterial.opacity = THREE.MathUtils.lerp(.64, 0, ease);
1186
+ for (const material of hatchMaterials) material.opacity = Math.max(.02, material.userData?.baseOpacity || .28) * (1 - ease * .98);
1187
+ for (const material of shadowMaterials) material.opacity = 1 - ease;
1188
+ for (const entry of dirtySlices) {
1189
+ const dirtyScale = THREE.MathUtils.lerp(1, .82, ease);
1190
+ entry.mesh.scale.set(dirtyScale, dirtyScale, dirtyScale);
1191
+ entry.mesh.position.x = THREE.MathUtils.lerp(entry.dirtyPosition.x, 0, ease);
1192
+ entry.mesh.position.y = entry.baseThickness / 2;
1193
+ entry.mesh.position.z = THREE.MathUtils.lerp(entry.dirtyPosition.z, 0, ease);
1194
+ entry.mesh.rotation.y = entry.dirtyRotation * (1 - ease);
1195
+ entry.mesh.rotation.x = entry.dirtyRotationX * (1 - ease);
1196
+ entry.mesh.rotation.z = entry.dirtyRotationZ * (1 - ease);
1197
+ entry.dirtyCylinder.castShadow = ease < .58;
1198
+ entry.mesh.visible = ease < .995;
1199
+ entry.cleanCity.visible = false;
1200
+ entry.authenticCity.visible = ease > .01;
1201
+ }
1202
+ for (const entry of repoLabels) {
1203
+ entry.sprite.position.y = THREE.MathUtils.lerp(entry.dirtyY, entry.authenticY, ease);
1204
+ }
1205
+ for (const piece of dirtyPieces) piece.scale.setScalar(Math.max(.001, 1 - ease));
1206
+ hemisphere.intensity = THREE.MathUtils.lerp(.88, .7, ease);
1207
+ ambient.intensity = THREE.MathUtils.lerp(.3, .2, ease);
1208
+ key.intensity = THREE.MathUtils.lerp(1.28, 2.3, ease);
1209
+ fill.intensity = .4 * ease;
1210
+ }
1211
+
1212
+ function resize() {
1213
+ const width = innerWidth;
1214
+ const height = innerHeight;
1215
+ renderer.setSize(width, height, false);
1216
+ const aspect = width / height;
1217
+ const vertical = width < 800 ? 16 : 12.6;
1218
+ camera.left = -vertical * aspect;
1219
+ camera.right = vertical * aspect;
1220
+ camera.top = vertical;
1221
+ camera.bottom = -vertical;
1222
+ camera.updateProjectionMatrix();
1223
+ }
1224
+ addEventListener("resize", resize);
1225
+ resize();
1226
+
1227
+ let visible = true;
1228
+ document.addEventListener("visibilitychange", () => { visible = !document.hidden; });
1229
+ function animate() {
1230
+ requestAnimationFrame(animate);
1231
+ if (!visible) return;
1232
+ updateRepair();
1233
+ controls.update();
1234
+ renderer.render(scene, camera);
1235
+ }
1236
+ animate();
1237
+ </script>
1238
+ </body>
1239
+ </html>