Files
Sophia/web/src/synapse_mesh.ts
dtoro 8688f632bf 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>
2026-04-28 08:29:37 +02:00

183 lines
6.4 KiB
TypeScript

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;
}
}