mirror.test.js 4.21 KB
// 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)
  })
})