Implement Sophia MVP scaffold (stages 0–3 + topology pivot)
Six-crate Rust workspace (core/sim/llm/store/server/bin) backing a Three.js + WebGL frontend. Live at http://127.0.0.1:7777 via `cargo run`. Sim - Event-driven scheduler with min-heap, per-engram tick, staggered Spawn events (40 ms apart) so each engram's flight is visually readable. - Solid-donut torus topology (replaces original spherical density-driven shell, see Topology Pivot in docs/system-analysis.md). Configurable major/minor radii in config.toml; live `POST /api/galaxy/:id/resize`. - Physics: Verlet integration + friction; in-hole pull + galactic spin (CCW around +z) for spiral-ejection ejection from the donut centre; soft tube boundary with velocity-reflecting wall. - Cosine-weighted gravity (kiddo k-NN within radius 25, threshold 0.50) and synapse formation (threshold 0.62) gated to inside-the-tube only. - LM Studio integration via OpenAI-compatible REST: batched embeddings, optional Bearer auth, semaphore-bounded parallel ops per §13.5. Server - axum HTTP + WebSocket. Routes: /healthz, /api/galaxy CRUD, /seed, /ingest, /resize, /engrams/:id, /ws/galaxy/:id/events. - Binary 12-byte-aligned position frames at ~20 Hz; JSON for sparse events (Hello, EngramCreated, SynapseCreated, TorusUpdated). - Layered config: config.toml (defaults) + config.local.toml (secrets, gitignored) merged on startup. Frontend - Vite + vanilla TypeScript + three.js 0.169. - Engrams render as additive bloom-friendly point sprites with a per-engram hash-driven hue rotation and breathing pulse. - Comet-style velocity-aligned trails; additive ribbon synapses whose endpoints track engram positions every frame. - UnrealBloomPass + ACES tone-mapping for the linked-particles look. - HUD shows torus dims, engram + synapse counts, LM Studio status; controls for seed, ingest, and live torus resize. Docs - README replaced with docs/IDEA.md; system-analysis.md updated with the topology pivot decisions and Galaxy Ejection refinement notes (the implementation plan lives in ~/.claude/plans, gitignored). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
182
web/src/synapse_mesh.ts
Normal file
182
web/src/synapse_mesh.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
import type { PositionFrame } from "./ws_client";
|
||||
|
||||
/**
|
||||
* Renders synapses (Stage 3) as additive line segments connecting two
|
||||
* engrams. Endpoints are looked up from the engram position buffer on every
|
||||
* position frame, so the lines track engram motion automatically.
|
||||
*
|
||||
* Uses a fixed-capacity vertex buffer; one segment per synapse → 2 vertices
|
||||
* per synapse → 6 floats of position per synapse.
|
||||
*/
|
||||
const VERT_SHADER = /* glsl */ `
|
||||
attribute float aAlpha;
|
||||
varying vec3 vColor;
|
||||
varying float vAlpha;
|
||||
void main() {
|
||||
vColor = color;
|
||||
vAlpha = aAlpha;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const FRAG_SHADER = /* glsl */ `
|
||||
varying vec3 vColor;
|
||||
varying float vAlpha;
|
||||
void main() {
|
||||
gl_FragColor = vec4(vColor, vAlpha);
|
||||
}
|
||||
`;
|
||||
|
||||
type SynapseEntry = {
|
||||
/** Slot in the line-segments geometry (0..capacity-1). */
|
||||
slot: number;
|
||||
/** instance_idx of the two engrams this connects. */
|
||||
aIdx: number;
|
||||
bIdx: number;
|
||||
weight: number;
|
||||
};
|
||||
|
||||
type Pending = {
|
||||
id: string;
|
||||
a: string;
|
||||
b: string;
|
||||
weight: number;
|
||||
};
|
||||
|
||||
export class SynapseMesh {
|
||||
private readonly mesh: THREE.LineSegments;
|
||||
private readonly positionAttr: THREE.BufferAttribute;
|
||||
private readonly colorAttr: THREE.BufferAttribute;
|
||||
private readonly alphaAttr: THREE.BufferAttribute;
|
||||
private readonly capacity: number;
|
||||
/** synapse_id → entry. */
|
||||
private readonly bySynapseId = new Map<string, SynapseEntry>();
|
||||
/** Engram UUID → instance_idx, populated as engrams arrive. */
|
||||
private readonly engramIdx = new Map<string, number>();
|
||||
/** Synapses waiting for one of their endpoints to be registered. */
|
||||
private readonly pending: Pending[] = [];
|
||||
/** Engram colour cache so we don't recompute on every frame. */
|
||||
private readonly engramColor: Float32Array;
|
||||
private nextSlot = 0;
|
||||
|
||||
constructor(scene: THREE.Scene, capacity = 8000) {
|
||||
this.capacity = capacity;
|
||||
this.engramColor = new Float32Array(5000 * 3);
|
||||
|
||||
const vertexCount = capacity * 2;
|
||||
const geom = new THREE.BufferGeometry();
|
||||
this.positionAttr = new THREE.BufferAttribute(new Float32Array(vertexCount * 3), 3);
|
||||
this.colorAttr = new THREE.BufferAttribute(new Float32Array(vertexCount * 3), 3);
|
||||
this.alphaAttr = new THREE.BufferAttribute(new Float32Array(vertexCount), 1);
|
||||
this.positionAttr.setUsage(THREE.DynamicDrawUsage);
|
||||
this.colorAttr.setUsage(THREE.DynamicDrawUsage);
|
||||
this.alphaAttr.setUsage(THREE.DynamicDrawUsage);
|
||||
geom.setAttribute("position", this.positionAttr);
|
||||
geom.setAttribute("color", this.colorAttr);
|
||||
geom.setAttribute("aAlpha", this.alphaAttr);
|
||||
geom.setDrawRange(0, 0);
|
||||
|
||||
const mat = new THREE.ShaderMaterial({
|
||||
vertexShader: VERT_SHADER,
|
||||
fragmentShader: FRAG_SHADER,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
});
|
||||
|
||||
this.mesh = new THREE.LineSegments(geom, mat);
|
||||
this.mesh.frustumCulled = false;
|
||||
scene.add(this.mesh);
|
||||
}
|
||||
|
||||
/** Register an engram so synapses referencing it can be wired up. */
|
||||
registerEngram(id: string, instanceIdx: number, r: number, g: number, b: number): void {
|
||||
this.engramIdx.set(id, instanceIdx);
|
||||
if (instanceIdx * 3 + 2 < this.engramColor.length) {
|
||||
this.engramColor[instanceIdx * 3] = r;
|
||||
this.engramColor[instanceIdx * 3 + 1] = g;
|
||||
this.engramColor[instanceIdx * 3 + 2] = b;
|
||||
}
|
||||
// Try to materialise any pending synapses now that this engram is known.
|
||||
if (this.pending.length > 0) {
|
||||
const stillPending: Pending[] = [];
|
||||
for (const p of this.pending) {
|
||||
if (!this.tryMaterialise(p)) stillPending.push(p);
|
||||
}
|
||||
this.pending.length = 0;
|
||||
this.pending.push(...stillPending);
|
||||
}
|
||||
}
|
||||
|
||||
/** Add a synapse by engram UUIDs. Defers if either endpoint is unknown. */
|
||||
addSynapse(id: string, a: string, b: string, weight: number): void {
|
||||
if (this.bySynapseId.has(id)) return;
|
||||
const p: Pending = { id, a, b, weight };
|
||||
if (!this.tryMaterialise(p)) {
|
||||
this.pending.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
private tryMaterialise(p: Pending): boolean {
|
||||
const aIdx = this.engramIdx.get(p.a);
|
||||
const bIdx = this.engramIdx.get(p.b);
|
||||
if (aIdx === undefined || bIdx === undefined) return false;
|
||||
if (this.nextSlot >= this.capacity) {
|
||||
console.warn("SynapseMesh capacity reached; ignoring further synapses");
|
||||
return true; // treat as resolved so we stop waiting on it
|
||||
}
|
||||
const slot = this.nextSlot++;
|
||||
this.bySynapseId.set(p.id, { slot, aIdx, bIdx, weight: p.weight });
|
||||
this.applyEndpointColors(slot, aIdx, bIdx);
|
||||
// Alpha tied to weight; a small floor so very weak synapses still register.
|
||||
const alpha = Math.max(0.08, Math.min(0.75, p.weight));
|
||||
const alphaArr = this.alphaAttr.array as Float32Array;
|
||||
alphaArr[slot * 2] = alpha;
|
||||
alphaArr[slot * 2 + 1] = alpha;
|
||||
this.alphaAttr.needsUpdate = true;
|
||||
this.mesh.geometry.setDrawRange(0, this.nextSlot * 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Apply the latest position frame to all synapse endpoints. */
|
||||
applyPositionFrame(frame: PositionFrame): void {
|
||||
const posArr = this.positionAttr.array as Float32Array;
|
||||
const src = frame.positions;
|
||||
const n = frame.n;
|
||||
let dirty = false;
|
||||
for (const entry of this.bySynapseId.values()) {
|
||||
const a = entry.aIdx, b = entry.bIdx;
|
||||
if (a >= n || b >= n) continue;
|
||||
const slot = entry.slot;
|
||||
posArr[slot * 6] = src[a * 3];
|
||||
posArr[slot * 6 + 1] = src[a * 3 + 1];
|
||||
posArr[slot * 6 + 2] = src[a * 3 + 2];
|
||||
posArr[slot * 6 + 3] = src[b * 3];
|
||||
posArr[slot * 6 + 4] = src[b * 3 + 1];
|
||||
posArr[slot * 6 + 5] = src[b * 3 + 2];
|
||||
dirty = true;
|
||||
}
|
||||
if (dirty) this.positionAttr.needsUpdate = true;
|
||||
}
|
||||
|
||||
/** Number of synapses currently rendered (for HUD). */
|
||||
count(): number {
|
||||
return this.bySynapseId.size;
|
||||
}
|
||||
|
||||
/** Re-paint a synapse's endpoint colours after one of its engrams updates. */
|
||||
private applyEndpointColors(slot: number, aIdx: number, bIdx: number): void {
|
||||
const colorArr = this.colorAttr.array as Float32Array;
|
||||
colorArr[slot * 6] = this.engramColor[aIdx * 3];
|
||||
colorArr[slot * 6 + 1] = this.engramColor[aIdx * 3 + 1];
|
||||
colorArr[slot * 6 + 2] = this.engramColor[aIdx * 3 + 2];
|
||||
colorArr[slot * 6 + 3] = this.engramColor[bIdx * 3];
|
||||
colorArr[slot * 6 + 4] = this.engramColor[bIdx * 3 + 1];
|
||||
colorArr[slot * 6 + 5] = this.engramColor[bIdx * 3 + 2];
|
||||
this.colorAttr.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user