The Phase 2 data-layer PRs modified these to import from data/ and core/, but those directories were removed in the Manus revert. Restore to the self-contained split-commit versions. panels/ and effects/ subdirectories were Phase 2 extractions not used by the main import chain (app.js -> modules/panels.js, not panels/).
263 lines
8.3 KiB
JavaScript
263 lines
8.3 KiB
JavaScript
// === FLOATING BOOKSHELVES + SPINE TEXTURES + COMMIT BANNERS ===
|
|
import * as THREE from 'three';
|
|
import { NEXUS } from './constants.js';
|
|
import { scene } from './scene-setup.js';
|
|
|
|
// === AGENT STATUS PANELS (declared early) ===
|
|
export const agentPanelSprites = [];
|
|
|
|
// === COMMIT BANNERS ===
|
|
export const commitBanners = [];
|
|
|
|
export const bookshelfGroups = [];
|
|
|
|
function createCommitTexture(hash, message) {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 512;
|
|
canvas.height = 64;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
ctx.fillStyle = 'rgba(0, 0, 16, 0.75)';
|
|
ctx.fillRect(0, 0, 512, 64);
|
|
|
|
ctx.strokeStyle = '#4488ff';
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeRect(0.5, 0.5, 511, 63);
|
|
|
|
ctx.font = 'bold 11px "Courier New", monospace';
|
|
ctx.fillStyle = '#4488ff';
|
|
ctx.fillText(hash, 10, 20);
|
|
|
|
ctx.font = '12px "Courier New", monospace';
|
|
ctx.fillStyle = '#ccd6f6';
|
|
const displayMsg = message.length > 54 ? message.slice(0, 54) + '\u2026' : message;
|
|
ctx.fillText(displayMsg, 10, 46);
|
|
|
|
return new THREE.CanvasTexture(canvas);
|
|
}
|
|
|
|
export async function initCommitBanners() {
|
|
let commits;
|
|
try {
|
|
const res = await fetch(
|
|
'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/commits?limit=5',
|
|
{ headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } }
|
|
);
|
|
if (!res.ok) throw new Error('fetch failed');
|
|
const data = await res.json();
|
|
commits = data.map(c => ({
|
|
hash: c.sha.slice(0, 7),
|
|
message: c.commit.message.split('\n')[0],
|
|
}));
|
|
} catch {
|
|
commits = [
|
|
{ hash: 'a1b2c3d', message: 'feat: depth of field effect on distant objects' },
|
|
{ hash: 'e4f5g6h', message: 'feat: photo mode with orbit controls' },
|
|
{ hash: 'i7j8k9l', message: 'feat: sovereignty easter egg animation' },
|
|
{ hash: 'm0n1o2p', message: 'feat: overview mode bird\'s-eye view' },
|
|
{ hash: 'q3r4s5t', message: 'feat: star field and constellation lines' },
|
|
];
|
|
|
|
initCommitBanners();
|
|
}
|
|
|
|
const spreadX = [-7, -3.5, 0, 3.5, 7];
|
|
const spreadY = [1.0, -1.5, 2.2, -0.8, 1.6];
|
|
const spreadZ = [-1.5, -2.5, -1.0, -2.0, -1.8];
|
|
|
|
commits.forEach((commit, i) => {
|
|
const texture = createCommitTexture(commit.hash, commit.message);
|
|
const material = new THREE.SpriteMaterial({
|
|
map: texture, transparent: true, opacity: 0, depthWrite: false,
|
|
});
|
|
const sprite = new THREE.Sprite(material);
|
|
sprite.scale.set(12, 1.5, 1);
|
|
sprite.position.set(
|
|
spreadX[i % spreadX.length],
|
|
spreadY[i % spreadY.length],
|
|
spreadZ[i % spreadZ.length]
|
|
);
|
|
sprite.userData = {
|
|
baseY: spreadY[i % spreadY.length],
|
|
floatPhase: (i / commits.length) * Math.PI * 2,
|
|
floatSpeed: 0.25 + i * 0.07,
|
|
startDelay: i * 2.5,
|
|
lifetime: 12 + i * 1.5,
|
|
spawnTime: null,
|
|
zoomLabel: `Commit: ${commit.hash}`,
|
|
};
|
|
scene.add(sprite);
|
|
commitBanners.push(sprite);
|
|
});
|
|
}
|
|
|
|
// === FLOATING BOOKSHELVES ===
|
|
function createSpineTexture(prNum, title, bgColor) {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 128;
|
|
canvas.height = 512;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
ctx.fillStyle = bgColor;
|
|
ctx.fillRect(0, 0, 128, 512);
|
|
|
|
ctx.strokeStyle = '#4488ff';
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeRect(3, 3, 122, 506);
|
|
|
|
ctx.font = 'bold 32px "Courier New", monospace';
|
|
ctx.fillStyle = '#4488ff';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(`#${prNum}`, 64, 58);
|
|
|
|
ctx.strokeStyle = '#4488ff';
|
|
ctx.lineWidth = 1;
|
|
ctx.globalAlpha = 0.4;
|
|
ctx.beginPath();
|
|
ctx.moveTo(12, 78);
|
|
ctx.lineTo(116, 78);
|
|
ctx.stroke();
|
|
ctx.globalAlpha = 1.0;
|
|
|
|
ctx.save();
|
|
ctx.translate(64, 300);
|
|
ctx.rotate(-Math.PI / 2);
|
|
const displayTitle = title.length > 30 ? title.slice(0, 30) + '\u2026' : title;
|
|
ctx.font = '21px "Courier New", monospace';
|
|
ctx.fillStyle = '#ccd6f6';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(displayTitle, 0, 0);
|
|
ctx.restore();
|
|
|
|
return new THREE.CanvasTexture(canvas);
|
|
}
|
|
|
|
function buildBookshelf(books, position, rotationY) {
|
|
const group = new THREE.Group();
|
|
group.position.copy(position);
|
|
group.rotation.y = rotationY;
|
|
|
|
const SHELF_W = books.length * 0.52 + 0.6;
|
|
const SHELF_THICKNESS = 0.12;
|
|
const SHELF_DEPTH = 0.72;
|
|
const ENDPANEL_H = 2.0;
|
|
|
|
const shelfMat = new THREE.MeshStandardMaterial({
|
|
color: 0x0d1520, metalness: 0.6, roughness: 0.5,
|
|
emissive: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.02),
|
|
});
|
|
|
|
const plank = new THREE.Mesh(new THREE.BoxGeometry(SHELF_W, SHELF_THICKNESS, SHELF_DEPTH), shelfMat);
|
|
group.add(plank);
|
|
|
|
const endGeo = new THREE.BoxGeometry(0.1, ENDPANEL_H, SHELF_DEPTH);
|
|
const leftEnd = new THREE.Mesh(endGeo, shelfMat);
|
|
leftEnd.position.set(-SHELF_W / 2, ENDPANEL_H / 2 - SHELF_THICKNESS / 2, 0);
|
|
group.add(leftEnd);
|
|
|
|
const rightEnd = new THREE.Mesh(endGeo.clone(), shelfMat);
|
|
rightEnd.position.set(SHELF_W / 2, ENDPANEL_H / 2 - SHELF_THICKNESS / 2, 0);
|
|
group.add(rightEnd);
|
|
|
|
const glowStrip = new THREE.Mesh(
|
|
new THREE.BoxGeometry(SHELF_W, 0.035, 0.035),
|
|
new THREE.MeshBasicMaterial({ color: NEXUS.colors.accent, transparent: true, opacity: 0.55 })
|
|
);
|
|
glowStrip.position.set(0, SHELF_THICKNESS / 2 + 0.017, SHELF_DEPTH / 2);
|
|
group.add(glowStrip);
|
|
|
|
const BOOK_COLORS = [
|
|
'#0f0818', '#080f18', '#0f1108', '#07120e',
|
|
'#130c06', '#060b12', '#120608', '#080812',
|
|
];
|
|
|
|
const bookStartX = -(SHELF_W / 2) + 0.36;
|
|
books.forEach((book, i) => {
|
|
const spineW = 0.34 + (i % 3) * 0.05;
|
|
const bookH = 1.35 + (i % 4) * 0.13;
|
|
const coverD = 0.58;
|
|
|
|
const bgColor = BOOK_COLORS[i % BOOK_COLORS.length];
|
|
const spineTexture = createSpineTexture(book.prNum, book.title, bgColor);
|
|
|
|
const plainMat = new THREE.MeshStandardMaterial({
|
|
color: new THREE.Color(bgColor), roughness: 0.85, metalness: 0.05,
|
|
});
|
|
const spineMat = new THREE.MeshBasicMaterial({ map: spineTexture });
|
|
|
|
const bookMats = [plainMat, plainMat, plainMat, plainMat, spineMat, plainMat];
|
|
|
|
const bookGeo = new THREE.BoxGeometry(spineW, bookH, coverD);
|
|
const bookMesh = new THREE.Mesh(bookGeo, bookMats);
|
|
bookMesh.position.set(bookStartX + i * 0.5, SHELF_THICKNESS / 2 + bookH / 2, 0);
|
|
bookMesh.userData.zoomLabel = `PR #${book.prNum}: ${book.title.slice(0, 40)}`;
|
|
group.add(bookMesh);
|
|
});
|
|
|
|
const shelfLight = new THREE.PointLight(NEXUS.colors.accent, 0.25, 5);
|
|
shelfLight.position.set(0, -0.4, 0);
|
|
group.add(shelfLight);
|
|
|
|
group.userData.zoomLabel = 'PR Archive — Merged Contributions';
|
|
group.userData.baseY = position.y;
|
|
group.userData.floatPhase = bookshelfGroups.length * Math.PI;
|
|
group.userData.floatSpeed = 0.17 + bookshelfGroups.length * 0.06;
|
|
|
|
scene.add(group);
|
|
bookshelfGroups.push(group);
|
|
}
|
|
|
|
export async function initBookshelves() {
|
|
let prs = [];
|
|
try {
|
|
const res = await fetch(
|
|
'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/pulls?state=closed&limit=20',
|
|
{ headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } }
|
|
);
|
|
if (!res.ok) throw new Error('fetch failed');
|
|
const data = await res.json();
|
|
prs = data
|
|
.filter(p => p.merged)
|
|
.map(p => ({
|
|
prNum: p.number,
|
|
title: p.title
|
|
.replace(/^\[[\w\s]+\]\s*/i, '')
|
|
.replace(/\s*\(#\d+\)\s*$/, ''),
|
|
}));
|
|
} catch {
|
|
prs = [
|
|
{ prNum: 324, title: 'Model training status — LoRA adapters' },
|
|
{ prNum: 323, title: 'The Oath — interactive SOUL.md reading' },
|
|
{ prNum: 320, title: 'Hermes session save/load' },
|
|
{ prNum: 304, title: 'Session export as markdown' },
|
|
{ prNum: 303, title: 'Procedural Web Audio ambient soundtrack' },
|
|
{ prNum: 301, title: 'Warp tunnel effect for portals' },
|
|
{ prNum: 296, title: 'Procedural terrain for floating island' },
|
|
{ prNum: 294, title: 'Northern lights flash on PR merge' },
|
|
];
|
|
}
|
|
|
|
// Duplicate podcast handler removed — it was in original but is handled in audio.js
|
|
// The original code had a duplicate podcast-toggle listener inside initBookshelves. Omitted.
|
|
|
|
document.getElementById('podcast-error').style.display = 'none';
|
|
|
|
if (prs.length === 0) return;
|
|
|
|
const mid = Math.ceil(prs.length / 2);
|
|
|
|
buildBookshelf(
|
|
prs.slice(0, mid),
|
|
new THREE.Vector3(-8.5, 1.5, -4.5),
|
|
Math.PI * 0.1,
|
|
);
|
|
|
|
if (prs.slice(mid).length > 0) {
|
|
buildBookshelf(
|
|
prs.slice(mid),
|
|
new THREE.Vector3(8.5, 1.5, -4.5),
|
|
-Math.PI * 0.1,
|
|
);
|
|
}
|
|
}
|