From 271a0dfa2f741c41c1b9c40a78b3b96600f51982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B0=D0=B5=D1=80=20=D0=9C=D0=B8=D1=85=D0=B0=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B8=D1=87?= Date: Thu, 27 Aug 2026 01:26:48 +0300 Subject: [PATCH] feat: refine Memento headset expressions --- src/features/avatar/avatars.ts | 89 +++++++++++++++++-- .../studio/__tests__/studio-document-test.ts | 47 ++++++++++ 2 files changed, 129 insertions(+), 7 deletions(-) diff --git a/src/features/avatar/avatars.ts b/src/features/avatar/avatars.ts index 829a426..c7c7344 100644 --- a/src/features/avatar/avatars.ts +++ b/src/features/avatar/avatars.ts @@ -318,11 +318,37 @@ const fixedSkinColorBehavior = (behavior?: AvatarBehaviorLibrary): AvatarBehavio return { expressions: [...expressions, ...scaredExpressions], sequences } } -const mementoBehavior = (behavior?: AvatarBehaviorLibrary): AvatarBehaviorLibrary => { +const mementoHeadsetWorkingExpressionId = 'memento-headset-working-joyful-eyes' +const mementoHeadsetShyExpressionId = 'memento-headset-shy-left' + +const copyExpressionEyes = (target: Expression, source: Expression): Expression => ({ + ...target, + widthLeft: source.widthLeft, + widthRight: source.widthRight, + heightLeft: source.heightLeft, + heightRight: source.heightRight, + curvatureLeft: source.curvatureLeft, + curvatureRight: source.curvatureRight, + spacing: source.spacing, + positionXLeft: source.positionXLeft, + positionXRight: source.positionXRight, + positionYLeft: source.positionYLeft, + positionYRight: source.positionYRight, + leftAngle: source.leftAngle, + rightAngle: source.rightAngle, + eyeMotion: source.eyeMotion, +}) + +const mementoBehavior = ( + behavior?: AvatarBehaviorLibrary, + customizeHeadsetExpressions = false +): AvatarBehaviorLibrary => { const sourceExpressions = (behavior?.expressions ?? initialExpressions).filter( expression => expression.semanticKey !== 'onboarding-listening' && - expression.semanticKey !== 'onboarding-curious' + expression.semanticKey !== 'onboarding-curious' && + expression.semanticKey !== mementoHeadsetWorkingExpressionId && + expression.semanticKey !== mementoHeadsetShyExpressionId ) const expressions = sourceExpressions.map(expression => { const fixedColorExpression = { ...expression } @@ -366,10 +392,53 @@ const mementoBehavior = (behavior?: AvatarBehaviorLibrary): AvatarBehaviorLibrar steps: sequence.steps.map(step => ({ ...step })), blink: { ...sequence.blink }, })) + const workingPose = expressions.find(expression => expression.semanticKey === 'angry-right') + const joyfulEyes = expressions.find(expression => expression.semanticKey === 'joyful-wide') + const headsetWorkingExpression = + customizeHeadsetExpressions && workingPose && joyfulEyes + ? { + ...copyExpressionEyes(workingPose, joyfulEyes), + id: mementoHeadsetWorkingExpressionId, + semanticKey: mementoHeadsetWorkingExpressionId, + } + : undefined + const shySource = expressions.find(expression => expression.semanticKey === 'upward-side-glance') + const headsetShyExpression = + customizeHeadsetExpressions && shySource + ? { + ...shySource, + id: mementoHeadsetShyExpressionId, + semanticKey: mementoHeadsetShyExpressionId, + headY: -Math.abs(shySource.headY), + spacing: shySource.spacing - 10, + positionYLeft: shySource.positionYLeft + 10, + positionYRight: shySource.positionYRight + 10, + } + : undefined + const sequenceFirstExpression = new Map([ + ...(headsetWorkingExpression ? [['working', headsetWorkingExpression.id] as const] : []), + ...(headsetShyExpression ? [['shy', headsetShyExpression.id] as const] : []), + ]) + const resolvedSequences = sequences.map(sequence => { + const expressionId = sequenceFirstExpression.get(sequence.semanticKey ?? '') + return expressionId + ? { + ...sequence, + steps: sequence.steps.map((step, index) => + index === 0 ? { ...step, expressionId } : step + ), + } + : sequence + }) return { - expressions: [...expressions, ...onboardingExpressions], + expressions: [ + ...expressions, + ...(headsetWorkingExpression ? [headsetWorkingExpression] : []), + ...(headsetShyExpression ? [headsetShyExpression] : []), + ...onboardingExpressions, + ], sequences: [ - ...sequences.filter(sequence => sequence.semanticKey !== 'onboarding'), + ...resolvedSequences.filter(sequence => sequence.semanticKey !== 'onboarding'), ...(onboardingExpressions.length === 2 ? [ { @@ -884,7 +953,7 @@ const primitiveBundledAvatars: StudioAvatar[] = [ ? { ...filledAvatarRenderStyle } : { ...defaultAvatarRenderStyle }, ...(surface === 'ghost' - ? { behavior: mementoBehavior() } + ? { behavior: mementoBehavior(undefined, id === 'primitive-ghost-headset') } : surface === 'apple-bite' ? { behavior: fixedSkinColorBehavior() } : surface === 'lock' @@ -1023,7 +1092,10 @@ export const ensurePrimitiveBundledAvatars = (library: AvatarLibrary): AvatarLib avatar.id ) ) { - return { ...avatar, behavior: mementoBehavior(avatar.behavior) } + return { + ...avatar, + behavior: mementoBehavior(avatar.behavior, avatar.id === 'primitive-ghost-headset'), + } } if (filledBundledAvatarIds.has(avatar.id)) { return { ...avatar, behavior: fixedSkinColorBehavior(avatar.behavior) } @@ -1177,7 +1249,10 @@ export const ensurePrimitiveBundledAvatars = (library: AvatarLibrary): AvatarLib ].includes(avatarWithAppleBiteEyes.id) ? { ...avatarWithAppleBiteEyes, - behavior: mementoBehavior(avatarWithAppleBiteEyes.behavior), + behavior: mementoBehavior( + avatarWithAppleBiteEyes.behavior, + avatarWithAppleBiteEyes.id === 'primitive-ghost-headset' + ), } : filledBundledAvatarIds.has(avatarWithAppleBiteEyes.id) ? { diff --git a/src/features/studio/__tests__/studio-document-test.ts b/src/features/studio/__tests__/studio-document-test.ts index 443a1d0..36ebffb 100644 --- a/src/features/studio/__tests__/studio-document-test.ts +++ b/src/features/studio/__tests__/studio-document-test.ts @@ -221,6 +221,53 @@ describe('Studio document', () => { expect(memento?.colors).toEqual({ body: '#ff9d45', eyes: '#111316' }) expect(headsetMemento?.colors).toEqual({ body: '#ff9d45', eyes: '#222222' }) expect(strokeMemento?.colors).toEqual({ body: '#ff9d45', eyes: '#ff9d45' }) + const headsetWorkingFirst = headsetMemento?.behavior?.sequences.find( + sequence => sequence.semanticKey === 'working' + )?.steps[0] + const headsetJoyfulFirst = headsetMemento?.behavior?.sequences.find( + sequence => sequence.semanticKey === 'joyful' + )?.steps[0] + const headsetShyFirst = headsetMemento?.behavior?.sequences.find( + sequence => sequence.semanticKey === 'shy' + )?.steps[0] + const headsetWorkingExpression = headsetMemento?.behavior?.expressions.find( + expression => expression.id === headsetWorkingFirst?.expressionId + ) + const headsetJoyfulExpression = headsetMemento?.behavior?.expressions.find( + expression => expression.id === headsetJoyfulFirst?.expressionId + ) + const headsetShyExpression = headsetMemento?.behavior?.expressions.find( + expression => expression.id === headsetShyFirst?.expressionId + ) + expect(headsetWorkingExpression).toMatchObject({ + widthLeft: headsetJoyfulExpression?.widthLeft, + widthRight: headsetJoyfulExpression?.widthRight, + heightLeft: headsetJoyfulExpression?.heightLeft, + heightRight: headsetJoyfulExpression?.heightRight, + spacing: headsetJoyfulExpression?.spacing, + leftAngle: headsetJoyfulExpression?.leftAngle, + rightAngle: headsetJoyfulExpression?.rightAngle, + }) + expect(headsetWorkingExpression).toMatchObject({ + headX: initialExpressions[7].headX, + headY: initialExpressions[7].headY, + headZ: initialExpressions[7].headZ, + }) + expect(headsetJoyfulFirst?.expressionId).toBe('expression-11') + expect(headsetShyExpression).toMatchObject({ + headY: -Math.abs(initialExpressions[0].headY), + spacing: initialExpressions[0].spacing - 10, + positionYLeft: initialExpressions[0].positionYLeft + 10, + positionYRight: initialExpressions[0].positionYRight + 10, + }) + expect( + memento?.behavior?.sequences.find(sequence => sequence.semanticKey === 'joyful')?.steps[0] + .expressionId + ).toBe('expression-11') + expect( + strokeMemento?.behavior?.sequences.find(sequence => sequence.semanticKey === 'joyful') + ?.steps[0].expressionId + ).toBe('expression-11') expect( strokeMemento?.behavior?.sequences.some(sequence => sequence.semanticKey === 'onboarding') ).toBe(true)