evaluator.js 3.74 KB
// evaluator.js —— createEvaluator:世界矩阵链式求值。移植自原型 evaluator.js + humanoid-v1.js 的 indexRig。
//
// 职责:吃一个 RigDefinition(来自 @drama/rig),把骨骼按「父先于子」拓扑排序,
// 然后给定一个 AnimationState,逐根 bone 算出世界矩阵(角色局部空间,root 在原点):
//   boneWorld = parentWorld × boneLocal,  boneLocal = fromTRS(setup+增量)。
// 不碰像素、不碰 slot 贴图——那是 @drama/render-pixi 的事,core 只产出 Map<boneName, Mat2D>。

import { fromTRS, mul, decompose, degToRad, PROP_DEFAULT, matrix } from './matrix.js'
import { AnimationState } from './anim-state.js'

/**
 * 把 rig 的 bones 按「父先于子」拓扑排序(保证世界变换可一次遍历算完),
 * 并建 name→bone 索引。移植自原型 indexRig。环会在 visit 中被 visited 截断(rig 保证无环)。
 * @param {import('./types.js').RigDefinition} rig
 * @returns {{boneByName:Map<string,import('./types.js').RigBone>, orderedBones:import('./types.js').RigBone[]}}
 */
export function indexRig(rig) {
  /** @type {Map<string, import('./types.js').RigBone>} */
  const boneByName = new Map()
  for (const b of rig.bones) boneByName.set(b.name, b)
  /** @type {import('./types.js').RigBone[]} */
  const ordered = []
  const visited = new Set()
  /** @param {import('./types.js').RigBone|undefined} b */
  const visit = (b) => {
    if (!b || visited.has(b.name)) return
    if (b.parent) visit(boneByName.get(b.parent))
    visited.add(b.name)
    ordered.push(b)
  }
  for (const b of rig.bones) visit(b)
  return { boneByName, orderedBones: ordered }
}

/**
 * 世界变换求值。
 * @param {{orderedBones:import('./types.js').RigBone[]}} rigIndex  indexRig 产物
 * @param {AnimationState} animState
 * @param {(Record<string,{drot?:number,dx?:number,dy?:number}>|null)=} overrides
 *        可选覆盖,叠加到动画采样之上(姿势编辑 / 微调用)。
 * @returns {Map<string, import('./types.js').Mat2D>}  角色局部空间,root 在原点
 */
export function evaluateWorld(rigIndex, animState, overrides = null) {
  const offsets = animState.sampleBones()
  /** @type {Map<string, import('./types.js').Mat2D>} */
  const world = new Map()
  for (const bone of rigIndex.orderedBones) {
    const off = offsets[bone.name] || PROP_DEFAULT
    const ov = overrides && overrides[bone.name]
    const rot = degToRad(bone.rotation + off.drot + (ov?.drot || 0))
    const x = bone.pos.x + off.dx + (ov?.dx || 0)
    const y = bone.pos.y + off.dy + (ov?.dy || 0)
    const scale = 1 * off.dscale
    const local = fromTRS(x, y, rot, scale)
    const parentW = bone.parent ? world.get(bone.parent) : null
    world.set(bone.name, parentW ? mul(parentW, local) : local)
  }
  return world
}

/**
 * 求值器工厂。
 * @param {import('./types.js').RigDefinition} rig
 * @param {import('./types.js').ClipLib=} clipLib  clip 库注入(默认应由装配层注入 @drama/rig)。
 * @returns {{
 *   state: () => AnimationState,
 *   evaluateWorld: (animState:AnimationState, overrides?:(Record<string,Object>|null)) => Map<string,import('./types.js').Mat2D>,
 *   matrix: typeof matrix,
 *   indexRig: () => {orderedBones:import('./types.js').RigBone[]}
 * }}
 */
export function createEvaluator(rig, clipLib) {
  const rigIndex = indexRig(rig)
  return {
    /** 每角色一个 AnimationState。 */
    state() {
      if (!clipLib) {
        throw new Error('createEvaluator: clipLib 未注入;state() 需要 clip 库(@drama/rig)')
      }
      return new AnimationState(clipLib)
    },
    evaluateWorld(animState, overrides = null) {
      return evaluateWorld(rigIndex, animState, overrides)
    },
    matrix,
    indexRig() {
      return rigIndex
    },
  }
}

export { decompose }