From 9114def84eab353a8f0349ee369dcc4f4596ff7f 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: Tue, 25 Aug 2026 19:16:41 +0300 Subject: [PATCH] fix: preserve dog ear depth across poses --- packages/avatar-core/src/geometry.ts | 4 +-- src/features/avatar/avatars.ts | 10 +++----- .../__tests__/rendered-scene-test.ts | 25 ++++++++++--------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/avatar-core/src/geometry.ts b/packages/avatar-core/src/geometry.ts index 84a8ac4..3b4ae41 100644 --- a/packages/avatar-core/src/geometry.ts +++ b/packages/avatar-core/src/geometry.ts @@ -1454,8 +1454,8 @@ const accessoryLayers = (pose: AvatarPose, nodes: BodyNode[]) => { depth, front: node.layer === 'front' || - (node.layer !== 'back' && - depth > accessoryCameraDepthRadius(pose, node) * ACCESSORY_FRONT_CROSSING_RATIO), + (node.layer !== 'back' && + depth > accessoryCameraDepthRadius(pose, node) * ACCESSORY_FRONT_CROSSING_RATIO), } }) .sort((left, right) => left.depth - right.depth) diff --git a/src/features/avatar/avatars.ts b/src/features/avatar/avatars.ts index ad23ffa..cc12557 100644 --- a/src/features/avatar/avatars.ts +++ b/src/features/avatar/avatars.ts @@ -195,11 +195,7 @@ export const simpleBundledAvatarIds = [ const simpleBundledAvatarOrder = new Map(simpleBundledAvatarIds.map((id, index) => [id, index])) // Remove hidden bundled characters from both fresh and previously persisted local libraries. -const hiddenBundledAvatarIds = new Set([ - 'memento-piglet', - 'oneworks-cat', - 'oneworks-cat-cow', -]) +const hiddenBundledAvatarIds = new Set(['memento-piglet', 'oneworks-cat', 'oneworks-cat-cow']) const retiredBundledAvatarIds = new Set([ 'memento-arctic-fox', @@ -579,13 +575,13 @@ const oneWorksDogAvatar = ( oneWorksNode('dog-ear-left', 'Left ear', 'teardrop', [-82, -65, 0], 0.18, 0.34, { scaleZ: 0.18, rotation: [-4, -10, 22], - layer: 'front', + layer: 'auto', color: earColor, }), oneWorksNode('dog-ear-right', 'Right ear', 'teardrop', [82, -65, 0], 0.18, 0.34, { scaleZ: 0.18, rotation: [-4, 10, -22], - layer: 'front', + layer: 'auto', color: earColor, }), ], diff --git a/src/features/rendering/__tests__/rendered-scene-test.ts b/src/features/rendering/__tests__/rendered-scene-test.ts index d102ea8..7b755da 100644 --- a/src/features/rendering/__tests__/rendered-scene-test.ts +++ b/src/features/rendering/__tests__/rendered-scene-test.ts @@ -98,18 +98,12 @@ describe('rendered avatar scene', () => { expect(forcedFront.frontNodeIds).toContain(avatar.body.nodes[0].id) }) - it('anchors dog ears in front of the head across expressions', () => { + it('moves dog ears between front and back with head depth', () => { const studio = loadStudioDocument({ getItem: () => null }) const avatar = studio.library.avatars.find(item => item.id === 'oneworks-dog')! const detachedEars: string[] = [] - const neutral = renderAvatar( - poseFromExpression(defaultExpression), - avatar.body.primary as SurfaceConfig, - 1, - { bodyNodes: avatar.body.nodes as BodyNode[] } - ) - expect(neutral.frontNodeIds).toEqual(['dog-ear-left', 'dog-ear-right']) + let splitDepthExpressions = 0 studio.expressions.forEach(expression => { const geometry = renderAvatar( @@ -121,11 +115,18 @@ describe('rendered avatar scene', () => { const head = pathBounds(geometry.headPath) const earIds = ['dog-ear-left', 'dog-ear-right'] as const + if ( + geometry.frontNodeIds.some(id => earIds.includes(id as (typeof earIds)[number])) && + geometry.backNodeIds.some(id => earIds.includes(id as (typeof earIds)[number])) + ) { + splitDepthExpressions += 1 + } earIds.forEach(nodeId => { const frontIndex = geometry.frontNodeIds.indexOf(nodeId) - expect(geometry.backNodeIds).not.toContain(nodeId) - expect(frontIndex).toBeGreaterThanOrEqual(0) - const earPath = geometry.frontPaths[frontIndex] + const backIndex = geometry.backNodeIds.indexOf(nodeId) + expect(Number(frontIndex >= 0) + Number(backIndex >= 0)).toBe(1) + const earPath = + frontIndex >= 0 ? geometry.frontPaths[frontIndex] : geometry.backPaths[backIndex] const ear = pathBounds(earPath) const headOverlap = overlap(ear, head) if (headOverlap.x <= 0 || headOverlap.y <= 0) { @@ -134,9 +135,9 @@ describe('rendered avatar scene', () => { ) } }) - }) + expect(splitDepthExpressions).toBeGreaterThan(0) expect(detachedEars, 'detached dog ears').toEqual([]) })