Skip to content

Commit 922c62e

Browse files
author
CI
committed
fix: resolve lint issues and refine avatar visuals
1 parent 239a08d commit 922c62e

16 files changed

Lines changed: 98 additions & 83 deletions

src/__tests__/properties/asr.property.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,11 @@ describe('ASR Service Properties', () => {
168168
const asr = new ASRService();
169169

170170
// Multiple starts should not throw
171-
let allSucceeded = true;
172171
for (let i = 0; i < startCount; i++) {
173172
try {
174173
await asr.start();
175174
} catch {
176175
// Errors are acceptable, but should not crash
177-
allSucceeded = false;
178176
}
179177
}
180178

@@ -242,4 +240,4 @@ describe('ASR Service Properties', () => {
242240
{ numRuns: 50 }
243241
);
244242
});
245-
});;
243+
});

src/__tests__/properties/performance.property.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ describe('Performance Properties', () => {
157157
fc.asyncProperty(
158158
fc.boolean(), // Is page visible
159159
fc.integer({ min: 50, max: 500 }), // Pause delay
160-
async (isVisible, pauseDelay) => {
160+
async (isVisible, _pauseDelay) => {
161161
let processingPaused = false;
162162
let pauseTime: number | null = null;
163163
const visibilityChangeTime = Date.now();
@@ -198,11 +198,8 @@ describe('Performance Properties', () => {
198198
it('VisibilityOptimizer calls pause callbacks when hidden', () => {
199199
const optimizer = new VisibilityOptimizer({ pauseDelay: 0, resumeDelay: 0 });
200200

201-
let pauseCalled = false;
202-
let resumeCalled = false;
203-
204-
optimizer.onPause(() => { pauseCalled = true; });
205-
optimizer.onResume(() => { resumeCalled = true; });
201+
optimizer.onPause(() => { });
202+
optimizer.onResume(() => { });
206203

207204
// Initially visible
208205
expect(optimizer.isVisible()).toBe(true);

src/__tests__/properties/store.property.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,6 @@ describe('Store Properties', () => {
114114
* disconnected → connecting → connected, or any state → error.
115115
*/
116116
it('Property 29: Store Connection State Transitions - follows valid paths', async () => {
117-
vi.resetModules();
118-
const { useDigitalHumanStore } = await import('../../store/digitalHumanStore');
119-
120117
const validTransitions: Record<string, string[]> = {
121118
'disconnected': ['connecting', 'error'],
122119
'connecting': ['connected', 'error', 'disconnected'],
@@ -160,9 +157,6 @@ describe('Store Properties', () => {
160157
* adding a new message SHALL remove the oldest message to maintain the limit.
161158
*/
162159
it('Property 30: Store Chat History Limit - maintains max length', async () => {
163-
vi.resetModules();
164-
const { useDigitalHumanStore } = await import('../../store/digitalHumanStore');
165-
166160
await fc.assert(
167161
fc.asyncProperty(
168162
fc.integer({ min: 5, max: 20 }),
@@ -204,9 +198,6 @@ describe('Store Properties', () => {
204198
* Additional: Error queue length limit
205199
*/
206200
it('Error queue respects max length', async () => {
207-
vi.resetModules();
208-
const { useDigitalHumanStore } = await import('../../store/digitalHumanStore');
209-
210201
await fc.assert(
211202
fc.asyncProperty(
212203
fc.array(fc.string({ minLength: 1, maxLength: 50 }), { minLength: 1, maxLength: 20 }),
@@ -236,9 +227,6 @@ describe('Store Properties', () => {
236227
* Additional: Dismiss error removes from queue
237228
*/
238229
it('Dismiss error removes specific error', async () => {
239-
vi.resetModules();
240-
const { useDigitalHumanStore } = await import('../../store/digitalHumanStore');
241-
242230
await fc.assert(
243231
fc.asyncProperty(
244232
fc.array(fc.string({ minLength: 1, maxLength: 50 }), { minLength: 2, maxLength: 5 }),

src/__tests__/properties/vision.property.test.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,13 @@ vi.mock('@mediapipe/pose', () => ({
4141

4242
describe('Vision Service Properties', () => {
4343
let mockStream: any;
44-
let mockVideo: any;
45-
4644
beforeEach(() => {
4745
mockStream = {
4846
getTracks: vi.fn(() => [
4947
{ stop: vi.fn(), kind: 'video' },
5048
]),
5149
};
5250

53-
mockVideo = {
54-
srcObject: null,
55-
play: vi.fn().mockResolvedValue(undefined),
56-
};
57-
5851
// Mock navigator.mediaDevices
5952
Object.defineProperty(navigator, 'mediaDevices', {
6053
writable: true,
@@ -134,8 +127,6 @@ describe('Vision Service Properties', () => {
134127
visionService.updateConfig({ emotionDebounceMs: debounceMs });
135128

136129
const emittedEmotions: string[] = [];
137-
const onEmotion = (emotion: any) => emittedEmotions.push(emotion);
138-
139130
// The debounce logic should filter rapid changes
140131
// Stable emotions (repeated 3+ times) should be emitted
141132
const stableEmotions = emotions.filter((e, i, arr) => {

src/__tests__/store.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
1+
import { describe, it, expect, beforeEach } from 'vitest';
22
import { useDigitalHumanStore } from '../store/digitalHumanStore';
33

44
// 每个测试前重置 store

src/components/DigitalHumanViewer.enhanced.tsx

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ function VisibilityOptimizer({
4949
onVisibilityChange?: (visible: boolean) => void;
5050
}) {
5151
const { gl, invalidate } = useThree();
52-
const animationLoopRef = useRef<((time: number) => void) | null>(null);
5352

5453
useEffect(() => {
5554
const handleVisibilityChange = () => {
@@ -111,13 +110,18 @@ const EMOTION_LIGHT_COLORS: Record<string, string> = {
111110
function CyberAvatar() {
112111
const group = useRef<THREE.Group>(null);
113112
const headGroupRef = useRef<THREE.Group>(null);
114-
const bodyGroupRef = useRef<THREE.Group>(null);
115113
const headRef = useRef<THREE.Mesh>(null);
116114
const leftEyeRef = useRef<THREE.Mesh>(null);
117115
const rightEyeRef = useRef<THREE.Mesh>(null);
118116
const mouthRef = useRef<THREE.Mesh>(null);
119117
const leftBrowRef = useRef<THREE.Mesh>(null);
120118
const rightBrowRef = useRef<THREE.Mesh>(null);
119+
const leftIrisRef = useRef<THREE.Mesh>(null);
120+
const rightIrisRef = useRef<THREE.Mesh>(null);
121+
const leftPupilRef = useRef<THREE.Mesh>(null);
122+
const rightPupilRef = useRef<THREE.Mesh>(null);
123+
const leftHighlightRef = useRef<THREE.Mesh>(null);
124+
const rightHighlightRef = useRef<THREE.Mesh>(null);
121125
const ringsRef = useRef<THREE.Group>(null);
122126
const bodyRef = useRef<THREE.Mesh>(null);
123127
const leftArmRef = useRef<THREE.Group>(null);
@@ -146,6 +150,8 @@ function CyberAvatar() {
146150
leftArmRotX: 0,
147151
rightArmRotX: 0,
148152
bodyScale: 1,
153+
eyeLookX: 0,
154+
eyeLookY: 0,
149155
});
150156

151157
const {
@@ -159,9 +165,9 @@ function CyberAvatar() {
159165

160166
useFrame((state) => {
161167
const t = state.clock.elapsedTime;
162-
const dt = state.clock.getDelta();
163168
const intensity = Math.max(0, Math.min(1, expressionIntensity ?? 1));
164169
const lerp = THREE.MathUtils.lerp;
170+
const clamp = THREE.MathUtils.clamp;
165171
const anim = animState.current;
166172

167173
// ---- 目标值初始化 ----
@@ -328,6 +334,36 @@ function CyberAvatar() {
328334
if (leftEyeRef.current) leftEyeRef.current.scale.y = anim.leftEyeScaleY;
329335
if (rightEyeRef.current) rightEyeRef.current.scale.y = anim.rightEyeScaleY;
330336

337+
const targetEyeLookX = clamp(mouse.current.x * 0.05 + Math.sin(t * 1.7) * 0.01, -0.06, 0.06);
338+
const targetEyeLookY = clamp(mouse.current.y * 0.035 + Math.sin(t * 1.3) * 0.008, -0.045, 0.045);
339+
anim.eyeLookX = lerp(anim.eyeLookX, targetEyeLookX, 0.12);
340+
anim.eyeLookY = lerp(anim.eyeLookY, targetEyeLookY, 0.12);
341+
342+
if (leftIrisRef.current) {
343+
leftIrisRef.current.position.x = -0.24 + anim.eyeLookX;
344+
leftIrisRef.current.position.y = anim.eyeLookY;
345+
}
346+
if (rightIrisRef.current) {
347+
rightIrisRef.current.position.x = 0.24 + anim.eyeLookX;
348+
rightIrisRef.current.position.y = anim.eyeLookY;
349+
}
350+
if (leftPupilRef.current) {
351+
leftPupilRef.current.position.x = -0.24 + anim.eyeLookX;
352+
leftPupilRef.current.position.y = anim.eyeLookY;
353+
}
354+
if (rightPupilRef.current) {
355+
rightPupilRef.current.position.x = 0.24 + anim.eyeLookX;
356+
rightPupilRef.current.position.y = anim.eyeLookY;
357+
}
358+
if (leftHighlightRef.current) {
359+
leftHighlightRef.current.position.x = -0.22 + anim.eyeLookX * 0.8;
360+
leftHighlightRef.current.position.y = 0.02 + anim.eyeLookY * 0.8;
361+
}
362+
if (rightHighlightRef.current) {
363+
rightHighlightRef.current.position.x = 0.26 + anim.eyeLookX * 0.8;
364+
rightHighlightRef.current.position.y = 0.02 + anim.eyeLookY * 0.8;
365+
}
366+
331367
// ---- 眉毛动画 ----
332368
let targetLeftBrowY = 0;
333369
let targetRightBrowY = 0;
@@ -531,16 +567,16 @@ function CyberAvatar() {
531567

532568
// 共享材质
533569
const skinMat = useMemo(() => (
534-
<meshPhysicalMaterial color="#e8edf5" metalness={0.3} roughness={0.2} clearcoat={1} clearcoatRoughness={0.05} envMapIntensity={2.5} />
570+
<meshPhysicalMaterial color="#e8edf5" metalness={0.25} roughness={0.18} clearcoat={1} clearcoatRoughness={0.05} envMapIntensity={2.8} />
535571
), []);
536572
const armorMat = useMemo(() => (
537-
<meshPhysicalMaterial color="#1a2332" metalness={0.9} roughness={0.1} clearcoat={1} clearcoatRoughness={0.03} envMapIntensity={2} />
573+
<meshPhysicalMaterial color="#151f2e" metalness={0.95} roughness={0.08} clearcoat={1} clearcoatRoughness={0.025} envMapIntensity={2.3} />
538574
), []);
539575
const frameMat = useMemo(() => (
540-
<meshStandardMaterial color="#3a4a5c" metalness={0.85} roughness={0.15} />
576+
<meshStandardMaterial color="#3f5266" metalness={0.8} roughness={0.18} />
541577
), []);
542578
const glowCyan = useMemo(() => (
543-
<meshStandardMaterial color="#22d3ee" emissive="#22d3ee" emissiveIntensity={3} toneMapped={false} />
579+
<meshStandardMaterial color="#22d3ee" emissive="#22d3ee" emissiveIntensity={3.2} toneMapped={false} />
544580
), []);
545581

546582
return (
@@ -598,29 +634,29 @@ function CyberAvatar() {
598634
<meshStandardMaterial color="#e2e8f0" metalness={0.1} roughness={0.3} />
599635
</mesh>
600636
{/* 虹膜 */}
601-
<mesh position={[-0.24, 0, 0.12]} scale={[1, 1, 0.3]}>
637+
<mesh ref={leftIrisRef} position={[-0.24, 0, 0.12]} scale={[1, 1, 0.3]}>
602638
<sphereGeometry args={[0.065, 24, 24]} />
603639
<meshStandardMaterial color="#38bdf8" emissive="#38bdf8" emissiveIntensity={2} toneMapped={false} />
604640
</mesh>
605-
<mesh position={[0.24, 0, 0.12]} scale={[1, 1, 0.3]}>
641+
<mesh ref={rightIrisRef} position={[0.24, 0, 0.12]} scale={[1, 1, 0.3]}>
606642
<sphereGeometry args={[0.065, 24, 24]} />
607643
<meshStandardMaterial color="#38bdf8" emissive="#38bdf8" emissiveIntensity={2} toneMapped={false} />
608644
</mesh>
609645
{/* 瞳孔 */}
610-
<mesh position={[-0.24, 0, 0.14]} scale={[1, 1, 0.2]}>
646+
<mesh ref={leftPupilRef} position={[-0.24, 0, 0.14]} scale={[1, 1, 0.2]}>
611647
<sphereGeometry args={[0.035, 16, 16]} />
612648
<meshStandardMaterial color="#0284c7" emissive="#22d3ee" emissiveIntensity={5} toneMapped={false} />
613649
</mesh>
614-
<mesh position={[0.24, 0, 0.14]} scale={[1, 1, 0.2]}>
650+
<mesh ref={rightPupilRef} position={[0.24, 0, 0.14]} scale={[1, 1, 0.2]}>
615651
<sphereGeometry args={[0.035, 16, 16]} />
616652
<meshStandardMaterial color="#0284c7" emissive="#22d3ee" emissiveIntensity={5} toneMapped={false} />
617653
</mesh>
618654
{/* 高光点 */}
619-
<mesh position={[-0.22, 0.02, 0.15]}>
655+
<mesh ref={leftHighlightRef} position={[-0.22, 0.02, 0.15]}>
620656
<sphereGeometry args={[0.012, 8, 8]} />
621657
<meshBasicMaterial color="#ffffff" />
622658
</mesh>
623-
<mesh position={[0.26, 0.02, 0.15]}>
659+
<mesh ref={rightHighlightRef} position={[0.26, 0.02, 0.15]}>
624660
<sphereGeometry args={[0.012, 8, 8]} />
625661
<meshBasicMaterial color="#ffffff" />
626662
</mesh>

src/components/VRMAvatar.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { useEffect, useRef, useState, useMemo } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
22
import { useFrame } from '@react-three/fiber';
33
import * as THREE from 'three';
44
import { VRM, VRMUtils } from '@pixiv/three-vrm';
55
import { useDigitalHumanStore } from '../store/digitalHumanStore';
66
import { useVRMLoader } from '../hooks/vrm/useVRMLoader';
7-
import { useVRMEmote } from '../hooks/vrm/useVRMEmote';
7+
import { useVRMEmote as createVRMEmote } from '../hooks/vrm/useVRMEmote';
88
import { useVRMBlink } from '../hooks/vrm/useVRMBlink';
99
import { useVRMLipSync } from '../hooks/vrm/useVRMLipSync';
1010
import { useVRMEyeSaccades } from '../hooks/vrm/useVRMEyeSaccades';
@@ -54,7 +54,7 @@ export default function VRMAvatar({ url, onLoad, onError, onProgress }: VRMAvata
5454
}, []);
5555

5656
// 使用单例 VRM 加载器
57-
const loader = useMemo(() => useVRMLoader(), []);
57+
const loader = useVRMLoader();
5858

5959
// 加载 VRM 模型
6060
useEffect(() => {
@@ -85,7 +85,7 @@ export default function VRMAvatar({ url, onLoad, onError, onProgress }: VRMAvata
8585
vrm.scene.position.set(0, -1.8, 0);
8686

8787
// 初始化模块化控制器
88-
emoteRef.current = useVRMEmote(vrm);
88+
emoteRef.current = createVRMEmote(vrm);
8989

9090
vrmRef.current = vrm;
9191
setLoaded(true);
@@ -116,7 +116,7 @@ export default function VRMAvatar({ url, onLoad, onError, onProgress }: VRMAvata
116116
vrmRef.current = null;
117117
}
118118
};
119-
}, [url]);
119+
}, [loader, onError, onLoad, onProgress, url]);
120120

121121
// ============================================================
122122
// 每帧更新 — 模块化:表情 + 眨眼 + 唇形 + 眼球微动 + 骨骼

src/components/VisionMirrorPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useEffect, useRef, useState, useCallback } from 'react';
2-
import { visionService, type VisionStatus } from '../core/vision/visionService';
2+
import { visionService } from '../core/vision/visionService';
33
import type { UserEmotion } from '../core/vision/visionMapper';
44
import { Camera, CameraOff, ScanFace, AlertCircle, Loader2 } from 'lucide-react';
55
import { toast } from 'sonner';

0 commit comments

Comments
 (0)