@faicad/animation
v0.2.0
Published
FAICAD animation: assembly schema + three.js scene builder + MIMIC runtime (define + play in one package)
Downloads
119
Readme
@faicad/animation
CAD 装配 → 浏览器实时动画,一个包搞定。
faijs 建模 → 装配定义 → three.js 嵌套枢轴树 → MIMIC 数值求值 → 实时播放。 定义与播放在同一个 TypeScript 工程里,没有 GLB 中间格式,没有服务端。
model.fai.js(几何 + 装配 + 运动学) → buildModel → three 场景树 → 播放
建模端(anim.* 子包) @faicad/animation(播放端)写模型脚本
建模端 = 一个 .fai.js:几何 + 零件装配 + 运动学全写在同一个脚本里
(对标 python 侧的 gen.py)。渲染它(buildModel)是播放端的事,
播放时长 duration 也属播放端,模型脚本里没有。
import * as anim from 'fai-animation'
let base = cad.cylinder(15 * MM, 8 * MM, { centered: true })
let arm = cad.box(60 * MM, 8 * MM, 8 * MM, { centered: true })
base.setColor('#555555') // 外观是几何对象的方法,不是装配 API
arm.setColor('#ff8800')
// 安装位姿的唯一来源:cad.assembly 约束解算(脚本不写任何安装位置)
let shoulder_asm = cad.assembly({
name: 'shoulder_asm',
members: [base, arm],
constraints: [
{ type: 'fixed', part: 'base' }, // 基座锚定在建模位姿
{ type: 'coincident', // 臂根铰点贴基座顶面 (0,0,4)
a: { part: 'arm', point: [0, 0, 0] },
b: { part: 'base', point: [0, 0, 4 * MM] } },
{ type: 'fixedRotation', part: 'arm' }, // 按最终姿态建模 → 钉恒等朝向
],
})
// 运动:anim.joint 只规定结构与运动参数;rest pose 由上面的装配解回填
anim.joint('shoulder', 'REVOLUTE', { // child 可传变量(等价 child: 'arm')
child: arm, axis: [0, 0, 1],
})
anim.declareMotions('wave', [anim.driver('shoulder', 90)])脚本三节形态:几何 cad.* → 安装 cad.assembly → 运动 anim.*。
anim.joint 不再接受 basePosition(传已删字段当场抛错)。
完整契约见 docs/faijs-script-contract.md
(anim.* 签名表、Shape 方法与颜色入参、faijs 方言的「能/不能」、单位与坐标、
安装位姿的约束配方、宿主调用序)。可直接运行的最小样例:demos/belt/model.fai.js。
写脚本时最容易踩的五条:颜色只收 hex 字符串或 0–1 三元组(数字
0xff8800抛错); 一行一条语句;顶层 op 实参里不能出现Math.*;cad.assembly(...)必须赋值给变量 (裸语句的结果不被收集);裸#注释行过不了解析器(用//)。
30 秒上手
import {
buildAssemblyScene, compileMotion, GSAPMotionController, MotionControlPanel,
} from '@faicad/animation'
import type { AssemblySpec, MotionSpec } from '@faicad/animation'
// 1. 装配拓扑:关节连接谁、绕哪根轴
const assembly: AssemblySpec = {
version: 2,
joints: [
{ name: 'pinion_joint', type: 'REVOLUTE', parent: 'root', child: 'pinion', axis: [0, 0, 1] },
{ name: 'rack_joint', type: 'PRISMATIC', parent: 'root', child: 'rack', axis: [0, 1, 0],
basePosition: [20, -62.83, 0] },
],
}
// 2. 运动行为:一个 DRIVER,其余 MIMIC
const motions: MotionSpec[] = [
{ name: 'pinion_joint', type: 'DRIVER', totalDegrees: 360 },
{ name: 'rack_joint', type: 'MIMIC',
inputs: [{ joint: 'pinion_joint', multiplier: Math.PI / 9 }] }, // πmz/360
]
// 3. 几何(faijs Shape → THREE.Mesh,这里假设已有)
const meshes = new Map([['pinion', pinionMesh], ['rack', rackMesh]])
// 4. 编译 + 建树
const { bundle, diagnostics } = compileMotion(assembly, motions, [], { duration: 4 })
if (diagnostics.length) console.warn(diagnostics)
const { root, nodeMap } = buildAssemblyScene({ meshes, assembly })
scene.add(root)
// 5. 播放
const controller = new GSAPMotionController(root, bundle, { nodeMap, autoPlay: true })
new MotionControlPanel({ container: document.getElementById('panel') })
.setController(controller)上手段演示的是播放层直用(宿主自带几何,不过模型脚本):basePosition /
baseRotation 是 AssemblyJoint 的 rest pose 字段,手写 spec 时由作者给;
走 buildModel(.fai.js 脚本路径)时这两个字段由 cad.assembly 约束解自动
回填,脚本里不写。
跑 demo:
npm install
npm run dev # → /demos/index.html两条动画路径
| 路径 | 驱动方式 | 判据 | 适用 |
|---|---|---|---|
| A — 时间驱动 | GSAP 时钟 → TimeDrivenJoint.update(t) | 有 angularRange / linearRange,无 mimic | 独立关节摆动、滑动、钟摆 |
| B — 数据流驱动 | MIMIC 引擎 + 同一时钟 | 有 mimic | 齿轮 / 皮带 / 凸轮 / 差速 / 行星轮系 |
写完整编译链时,DRIVER 也会被编译成 mimic: { inputs: [{joint: '_clock'}] },
所以走 compileMotion() 的场景里全部关节都在 Path B。Path A 只在直接喂手写
bundle(历史 v1 .motion.json)时出现。
机构约束:传动比不手填
compileMotion() 接受声明式的 Coupling[],编译成线性约束方程组后高斯消元,
解出每个关节相对 driver 的速比,自动填进 mimic.inputs[].multiplier。
compileMotion(assembly, motions, [
{ kind: 'gearMesh', a: 'sun', b: 'planet', teethA: 18, teethB: 12, carrier: 'carrier' },
{ kind: 'gearMesh', a: 'planet', b: 'ring', teethA: 12, teethB: 42, internal: true, carrier: 'carrier' },
{ kind: 'fixed', joint: 'carrier' },
], { duration: 4 })以 ring 为基准解出 sun = −2.3333 (= −Zr/Zs)、planet = 3.5 (= +Zr/Zp)、
carrier = 0——与手填系数一致,但不用手算。
支持的 coupling:fixed · gearMesh(外/内啮合,可带 carrier)· belt ·
rackPinion · cam(走 curve,不进方程组)· differential。
显式写的 MIMIC 优先级高于耦合推导的结果。约束欠定 / 过定会产生
diagnostics,但不阻断编译。
目录结构
animation/
src/
index.ts 统一导出
schema/
assembly.ts AssemblySpec — 装配拓扑(作者层)
motion.ts MotionSpec / Coupling(作者层)
bundle.ts MotionBundle — 播放层契约 + v1/v2 兼容
scene/
scene-builder.ts 装配 → 嵌套 pivot 场景树
shape-to-mesh.ts faijs Shape → THREE.Mesh
faijs-runtime.ts faijs 脚本执行封装(浏览器 + node 双环境)
fai-model-builder.ts buildModel(script):执行 .fai.js → mesh + 装配 + 运动学
fai-animation/ faijs 扩展子包:`anim.*` 声明装配与运动学(LibLoader 注入)
compile/
motion-compile.ts MotionSpec + Coupling → MotionBundle
gear-ratios.ts coupling → 约束方程
gaussian-elimination.ts 约束方程组求解
runtime/
controller.ts GSAPMotionController + TimeDrivenJoint
joint-driver.ts JointDriver(MIMIC 求值)
control-panel.ts MotionControlPanel(自包含播放面板)
loader/
glb-loader.ts 可选:读历史 GLB + FAICAD_motion 扩展
demos/ 7 个可播放案例 + 共享骨架
test/ vitest 单测 + playwright e2e + GLB fixture
docs/ 架构与契约文档
scripts/ci.ps1 本地一键 CIdemos
npm run dev 后打开 /demos/index.html。
| Demo | 特性 |
|---|---|
| planetary_gear | 行星架公转,18/12/42 真 BREP 渐开线齿轮,Case1/Case2 |
| planetary_gear2 | 行星架固定(背板支架),单 DRIVER + MIMIC 链 |
| rack_pinion | 旋转 → 直线,πmz/360 推导系数 |
| belt | 皮带 MIMIC 减速,轮上标记验证相位与转向 |
| cam | 凸轮曲线表 → 挺杆位移,曲线插值 |
| differential | 差速器双输入 MIMIC 叠加 |
| walk_swing | Path A 纯时间驱动:重力余弦 + 往返三角波 |
建模端 = 一个 .fai.js:几何 + 零件装配 + 运动学全写在脚本里
(对标 python 侧的 gen.py)。渲染它(buildModel)是播放端的事,
播放时长 duration 也属播放端,模型脚本里没有。
每个 demo 目录四个文件:
demos/<name>/
model.fai.js 建模端唯一文件:cad.* 出几何 + cad.assembly 定安装 + anim.joint/driver/mimic/declareMotions 定运动
demo.ts 只 re-export:modelScript(?raw 导入)+ duration + title/subtitle
page.ts 调 mountDemoPage(),声明标题 / 变体 / 取景
index.html 只 import ./page.tsURL 参数(供 e2e / 截图取证):?variant=case2 · ?t=2.5 定格 · ?zoom=1.6 · ?nogrid=1。
坐标系
Z-up · 角度 度 · 长度 mm · 默认旋转轴 [0, 0, 1]。
axis 定义在子节点局部坐标系下。
读历史 GLB(兼容路径)
本包不产 GLB,但能读 animation_gen(python)时代生成的 GLB +
FAICAD_motion 扩展 / 独立 .motion.json:
import { loadGlb } from '@faicad/animation/loader' // 可选入口,需显式引入
const { root, meta } = await loadGlb('/model.glb', '/model.motion.json')
const ctrl = new GSAPMotionController(root, meta)./loader 不在主入口的导出面上。红线:3d_editor 不引入此入口。
test/fixtures/glb/ 里是真实的 python 产物 fixture,test/dual-compat.test.ts
持续验证双兼容。
脚本
| 命令 | 作用 |
|---|---|
| npm run dev | vite dev server,开 /demos/index.html |
| npm run build | tsc → dist/(库产物) |
| npm run build:site | vite MPA 构建 demos 静态站 → preview-dist/ |
| npm run serve:site | 静态伺服 preview-dist/ |
| npm test | vitest 单测 |
| npm run test:e2e | playwright e2e(会自动起 vite :4173) |
本地 CI
pwsh scripts/ci.ps1按顺序跑 tsc → vitest → playwright,任一步失败即报 fail 并继续后续步骤,
最后汇总。GitHub Actions 见 .github/workflows/ci.yml,
跑同样的命令链。
依赖
| 包 | 角色 |
|---|---|
| three >=0.160.0 | peer — 场景图、四元数 / 向量 |
| gsap >=3.12.0 | peer — 动画时钟 |
| @faicad/faijs >=0.33.6 | peer — 建模引擎(src/scene/faijs-*):Shape / cad.* / 外观 / CadQuery 选择器 |
| @faicad/faijs-gears >=0.33.4 <0.34.5 | peer(可选)— BREP 渐开线齿轮 |
@faicad/faijs 与齿轮库从本地 faijs 兄弟仓的 tgz 安装(file: 依赖,
与 3d_editor 同线)。
齿轮库的版本上限与宿主契约:
@faicad/faijs-gears0.34.3 起是平台库(occt-only), 入口经gearKernel()取双句柄——l1 = getBrepApi()(宿主注入的 L1 适配器)+occt = getOcctKernel()(occt 平台面,补 L1 无法表达的loft壳形态 /healWire/split/projectPointOnFace)。宿主经registerOcctBrepEngine()登记 occt 引擎后, 两面同时可用且config.brepEngineId='occt',齿轮正常产出 BREP solid;未登记 occt 引擎时gearKernel()在执行前抛E_BREP_UNSUPPORTED(不补桩、不静默降级)。上限>=0.33.4 <0.34.5由test/gears-brep.test.ts把守:各用例先经生产宿主路径executeFaijsScript引导,再直调库入口读原始Result。
runtime/ 只依赖 three + gsap,不碰 faijs——宿主自带几何可以直接用播放层。
BREP 路径需要 occt-wasm,浏览器和 node(vitest)两条路都支持
(src/scene/faijs-runtime.ts 自动分流)。
文档
长期有效的内容文档:
| 文档 | 内容 |
|---|---|
| docs/faijs-script-contract.md | fai.js 脚本接口契约:anim.* 签名、Shape 方法、方言约束、宿主调用序 |
| docs/motion-contract.md | 三层数据契约、Coupling 约束方程、编译流程、v1/v2 兼容 |
| docs/runtime-architecture.md | 控制器 / MIMIC 引擎 / 播放面板的内部实现 |
| docs/auto-pivot.md | 装配 → 枢轴树的自动插入规则与约定 |
| docs/coordinate-system.md | Z-up 决策与落地位置 |
| docs/lessons/rack-pinion-bugs.md | 齿条齿轮的 4 个历史 bug 与当前待办 |
| SKILL.md | AI 助手上的速查卡 |
开发计划与方案(过程文档,非当前契约)在 docs/plans/:
| 文档 | 内容 |
|---|---|
| docs/plans/faijs-frontend-motion-plan.md | V2 技术方案(本仓的存在理由) |
| docs/plans/2026-10-14-plan-api-boundary-and-script-contract.md | 装配/外观 API 边界与脚本契约(2026-10-14 方案) |
| docs/plans/2026-10-15-plan-faijs-assembly-junction.md | 安装归 faijs cad.assembly、运动归 animation:结合点重构(2026-10-15 方案,已执行完毕) |
