Skip to content

Commit 6dcf77c

Browse files
committed
modularize rig code
1 parent 578bd5d commit 6dcf77c

18 files changed

Lines changed: 1149 additions & 1159 deletions

src/avatar/daemon-avatar-rig.ts

Lines changed: 10 additions & 1159 deletions
Large diffs are not rendered by default.

src/avatar/rig/core/rig-engine.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { THREE } from "@opentui/core/3d";
2+
import type { AvatarColorTheme } from "src/types";
3+
import { type SceneElements, createSceneElements } from "../scene/create-scene-elements";
4+
import { type RigState, createInitialState } from "../state/rig-state";
5+
import { updateThemeColors } from "../theme/rig-theme";
6+
import { TOOL_CATEGORY_COLORS, type ToolCategory } from "../tools/rig-tools";
7+
import { updateCore } from "../update/update-core";
8+
import { updateEye } from "../update/update-eye";
9+
import { updateFragments } from "../update/update-fragments";
10+
import { updateGlitchBehavior } from "../update/update-glitch";
11+
import { updateIdleAmbience } from "../update/update-idle";
12+
import { updateIntensityAndAudio } from "../update/update-intensity";
13+
import { updateMainAnchor } from "../update/update-main-anchor";
14+
import { updateParticles } from "../update/update-particles";
15+
import { updateRings } from "../update/update-rings";
16+
import { updateSigils } from "../update/update-sigils";
17+
import { clamp01 } from "../utils/math";
18+
import type { RigEngineOptions, RigEvent } from "./rig-types";
19+
20+
export class RigEngine {
21+
public readonly scene: THREE.Scene;
22+
public readonly camera: THREE.PerspectiveCamera;
23+
private readonly elements: SceneElements;
24+
private readonly state: RigState;
25+
private readonly disposables: Array<{ dispose(): void }> = [];
26+
27+
constructor(options: RigEngineOptions) {
28+
this.scene = new THREE.Scene();
29+
this.camera = new THREE.PerspectiveCamera(28, options.aspectRatio, 0.1, 100);
30+
this.camera.position.set(0, 0, 8);
31+
this.camera.lookAt(0, 0, 0);
32+
33+
const trackGeo = <T extends THREE.BufferGeometry>(g: T): T => {
34+
this.disposables.push(g);
35+
return g;
36+
};
37+
const trackMat = <T extends THREE.Material>(m: T): T => {
38+
this.disposables.push(m);
39+
return m;
40+
};
41+
42+
this.elements = createSceneElements(this.scene, trackGeo, trackMat);
43+
this.state = createInitialState();
44+
}
45+
46+
public getScene(): THREE.Scene {
47+
return this.scene;
48+
}
49+
50+
public getCamera(): THREE.PerspectiveCamera {
51+
return this.camera;
52+
}
53+
54+
public update(deltaS: number): void {
55+
const dt = Math.min(0.1, deltaS);
56+
this.state.glitch.timer += dt;
57+
58+
const intensity = updateIntensityAndAudio(this.state, dt);
59+
const isIdle = intensity < 0.1;
60+
const allowGlitch = intensity > 0.4 && !this.state.reasoning.active;
61+
62+
updateMainAnchor(this.elements, this.state, dt, intensity);
63+
updateCore(this.elements, this.state, dt, intensity);
64+
updateEye(this.elements, this.state, dt, intensity);
65+
updateRings(this.elements, this.state, dt, intensity);
66+
updateFragments(this.elements, this.state, dt, intensity);
67+
updateSigils(this.elements, this.state, dt, intensity);
68+
updateParticles(this.elements, this.state, dt, intensity, allowGlitch);
69+
updateGlitchBehavior(this.elements, this.state, dt, intensity, allowGlitch);
70+
updateIdleAmbience(this.elements, this.state, dt, isIdle);
71+
updateThemeColors(this.elements, this.state, dt);
72+
}
73+
74+
public handle(event: RigEvent): void {
75+
switch (event.type) {
76+
case "theme":
77+
this.setTheme(event.theme);
78+
break;
79+
case "intensity":
80+
this.setIntensity(event.intensity, { immediate: event.immediate });
81+
break;
82+
case "audio":
83+
this.setAudioLevel(event.level, { immediate: event.immediate });
84+
break;
85+
case "tool-active":
86+
this.setToolActive(event.active, event.category);
87+
break;
88+
case "tool-flash":
89+
this.triggerToolFlash(event.category);
90+
break;
91+
case "tool-complete":
92+
this.triggerToolComplete();
93+
break;
94+
case "reasoning":
95+
this.setReasoningMode(event.active);
96+
break;
97+
case "typing":
98+
this.setTypingMode(event.active);
99+
break;
100+
case "typing-pulse":
101+
this.triggerTypingPulse();
102+
break;
103+
default:
104+
break;
105+
}
106+
}
107+
108+
public setTheme(theme: AvatarColorTheme): void {
109+
this.state.theme.target = { ...theme };
110+
}
111+
112+
public setColors(theme: AvatarColorTheme): void {
113+
this.setTheme(theme);
114+
}
115+
116+
public setIntensity(intensity: number, options?: { immediate?: boolean }): void {
117+
const next = clamp01(intensity);
118+
if (options?.immediate) {
119+
this.state.intensity.target = next;
120+
this.state.intensity.current = next;
121+
} else {
122+
if (next > this.state.intensity.target + 0.1) {
123+
this.state.intensity.spinBoost = 12.0;
124+
}
125+
this.state.intensity.target = next;
126+
}
127+
}
128+
129+
public setAudioLevel(level: number, options?: { immediate?: boolean }): void {
130+
const next = clamp01(level);
131+
if (options?.immediate) {
132+
this.state.audio.target = next;
133+
this.state.audio.current = next;
134+
} else {
135+
this.state.audio.target = next;
136+
}
137+
}
138+
139+
public setToolActive(active: boolean, category?: ToolCategory): void {
140+
this.state.tool.active = active;
141+
if (active && category) {
142+
this.elements.sigilMat.color.setHex(TOOL_CATEGORY_COLORS[category]);
143+
} else {
144+
this.elements.sigilMat.color.setHex(this.state.theme.current.primary);
145+
}
146+
}
147+
148+
public triggerToolFlash(category?: ToolCategory): void {
149+
this.state.tool.flashColor = category ? TOOL_CATEGORY_COLORS[category] : 0xffffff;
150+
this.state.tool.flashTimer = 0.15;
151+
this.state.tool.fragmentScatterBoost = 0.3;
152+
this.state.intensity.spinBoost = Math.max(this.state.intensity.spinBoost, 8);
153+
}
154+
155+
public triggerToolComplete(): void {
156+
this.state.tool.settleTimer = 0.2;
157+
}
158+
159+
public setReasoningMode(active: boolean): void {
160+
this.state.reasoning.active = active;
161+
}
162+
163+
public setTypingMode(active: boolean): void {
164+
this.state.typing.active = active;
165+
}
166+
167+
public triggerTypingPulse(): void {
168+
this.state.typing.pulse = Math.min(1.0, this.state.typing.pulse + 0.3);
169+
this.state.intensity.spinBoost = Math.max(this.state.intensity.spinBoost, 1.5);
170+
}
171+
172+
public dispose(): void {
173+
this.disposables.forEach((d) => d.dispose());
174+
}
175+
}

src/avatar/rig/core/rig-types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { AvatarColorTheme } from "src/types";
2+
import type { ToolCategory } from "../tools/rig-tools";
3+
4+
export interface RigEngineOptions {
5+
aspectRatio: number;
6+
}
7+
8+
export type RigEvent =
9+
| { type: "theme"; theme: AvatarColorTheme }
10+
| { type: "intensity"; intensity: number; immediate?: boolean }
11+
| { type: "audio"; level: number; immediate?: boolean }
12+
| { type: "tool-active"; active: boolean; category?: ToolCategory }
13+
| { type: "tool-flash"; category?: ToolCategory }
14+
| { type: "tool-complete" }
15+
| { type: "reasoning"; active: boolean }
16+
| { type: "typing"; active: boolean }
17+
| { type: "typing-pulse" };

0 commit comments

Comments
 (0)