// mirror.test.js — facing 翻转 + headFacing 扭头反射轴镜像(render-pixi §8 验收 5)。 // // 对照原型 character-actor.js: // - 贴图基准朝向=屏幕左 → facing='right' 才翻转根容器 scale.x(bodyFlipSign)。 // - headFacing≠facing → 头/脸 scale.x 翻转,且位置绕头骨节点 x 反射(reflectHeadX,不错位)。 import { describe, it, expect } from 'vitest' import { bodyFlipSign, headMirrorSign, reflectHeadX, resolveSlotWrite, } from '../src/geom.js' import { createPixiRenderer } from '../src/index.js' import { makeStubPixi, StubApplication } from './stub-pixi.js' describe('geom 纯几何', () => { it('bodyFlipSign:left 不翻(1)、right 翻(-1)', () => { expect(bodyFlipSign('left')).toBe(1) expect(bodyFlipSign('right')).toBe(-1) }) it('headMirrorSign:headFacing 与 facing 不同→-1,否则 1', () => { expect(headMirrorSign('left', null)).toBe(1) expect(headMirrorSign('left', 'left')).toBe(1) expect(headMirrorSign('left', 'right')).toBe(-1) expect(headMirrorSign('right', 'left')).toBe(-1) }) it('reflectHeadX:headMirror=-1 时绕反射轴镜像,=1 时原样', () => { expect(reflectHeadX(30, 50, -1)).toBe(70) // 2*50-30 expect(reflectHeadX(30, 50, 1)).toBe(30) }) it('resolveSlotWrite:head slot 扭头镜像翻转 scaleX 且反射位置', () => { const t = { x: 30, y: 10, rotation: 0, scaleX: 1, scaleY: 1 } const w = resolveSlotWrite(t, { isHeadSlot: true, headMirror: -1, headBoneX: 50 }) expect(w.scaleX).toBe(-1) expect(w.x).toBe(70) expect(w.y).toBe(10) }) it('resolveSlotWrite:非 head slot 不受扭头影响', () => { const t = { x: 30, y: 10, rotation: 0, scaleX: 1, scaleY: 1 } const w = resolveSlotWrite(t, { isHeadSlot: false, headMirror: -1, headBoneX: 50 }) expect(w.scaleX).toBe(1) expect(w.x).toBe(30) }) it('resolveSlotWrite:mirror 与扭头镜像叠加(-1×-1=1)', () => { const t = { x: 30, y: 10, rotation: 0, scaleX: 1, scaleY: 1, mirror: true } const w = resolveSlotWrite(t, { isHeadSlot: true, headMirror: -1, headBoneX: 50 }) expect(w.scaleX).toBe(1) // mirror -1,再 headMirror -1 }) }) const rig = { id: 'humanoid_v1', canvas: { width: 1080, height: 1080 }, bones: [{ name: 'root', parent: null, pos: { x: 0, y: 0 }, rotation: 0 }], slots: [ { name: 'head', bone: 'root', zOrder: 20 }, { name: 'face', bone: 'root', zOrder: 21 }, ], } describe('applyFacing 集成', () => { function setup(facing) { const PIXI = makeStubPixi() const r = createPixiRenderer(new StubApplication(), { PIXI, rig }) r.addActor('h', { rigId: 'humanoid_v1', parts: { head: { url: 'h.png' }, face: { url: 'f.png' } } }, { x: 0, y: 0, facing, scale: 1 }) return { r, actor: r._actors.get('h') } } it('facing=right 翻转根容器 scale.x(贴图基准=左)', () => { const { actor } = setup('right') expect(actor.root.scale.x).toBeLessThan(0) const { actor: a2 } = setup('left') expect(a2.root.scale.x).toBeGreaterThan(0) }) it('headFacing≠facing:已落地的 head/face 位置绕头骨反射、scale.x 翻转', () => { const { r, actor } = setup('left') // 先落一帧 pose,head 在 x=30 r.applyActorPose('h', { head: { x: 30, y: 0, rotation: 0, scaleX: 1, scaleY: 1 }, face: { x: 40, y: 5, rotation: 0, scaleX: 1, scaleY: 1 }, }) expect(actor.parts.get('head').obj.position.x).toBe(30) // 扭头看右(与身体 left 不同)→ 反射轴=head.x=30 r.applyFacing('h', 'left', 'right') const head = actor.parts.get('head').obj const face = actor.parts.get('face').obj expect(head.scale.x).toBe(-1) expect(head.position.x).toBe(30) // 2*30-30 expect(face.position.x).toBe(20) // 2*30-40,绕头骨反射不错位 expect(face.scale.x).toBe(-1) }) it('headFacing 回 null:恢复跟随身体(不再镜像)', () => { const { r, actor } = setup('left') r.applyActorPose('h', { head: { x: 30, y: 0, rotation: 0, scaleX: 1, scaleY: 1 } }) r.applyFacing('h', 'left', 'right') r.applyFacing('h', 'left', null) expect(actor.parts.get('head').obj.scale.x).toBe(1) expect(actor.parts.get('head').obj.position.x).toBe(30) }) })