From 474591c36b685890e2d0f7ca422521c68972edd4 Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Wed, 10 Jun 2026 11:14:34 +0800 Subject: [PATCH 1/7] =?UTF-8?q?docs(spec):=20v1.0=20sub-stage=20A2=20?= =?UTF-8?q?=E2=80=94=20Babylon=20behaviors=20=E8=B7=A8=E5=BC=95=E6=93=8E?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=20=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- ...6-06-10-v1.0a2-babylon-behaviors-design.md | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-10-v1.0a2-babylon-behaviors-design.md diff --git a/docs/superpowers/specs/2026-06-10-v1.0a2-babylon-behaviors-design.md b/docs/superpowers/specs/2026-06-10-v1.0a2-babylon-behaviors-design.md new file mode 100644 index 0000000..0a19c02 --- /dev/null +++ b/docs/superpowers/specs/2026-06-10-v1.0a2-babylon-behaviors-design.md @@ -0,0 +1,233 @@ +# v1.0 sub-stage A2 — Babylon behaviors 跨引擎运行时 设计 + +**状态**: 已批准(2026-06-10) +**前置**: v1.0a A1(多适配器契约 + Babylon headless + conformance,#37)已合并。`IRuntimeAdapter` 含引擎中立 `describeNode(): RuntimeNodeInfo` + `dispose()`;`BabylonAdapter`(`@babylonjs/core` `NullEngine`,headless)实现 syncNode 核心 kind;conformance 套件(`src/runtime/conformance/conformance-suite.ts`)参数化跑双适配器。 +**定位**: v1.0「多适配器」第二个 sub-stage。验证 **behavior 运行时执行**对第二引擎成立。 +**架构依据**: 架构 §7 v1.0 + §4.1 适配器契约 + §7 v0.5 行为系统。 + +--- + +## 0. 上下文与发现(实施前已读真实代码) + +- **behavior 运行时三方法已是引擎中立签名,但不在契约里**:`ThreeAdapter` 有 `installBehaviors(nodeId, bindings)` / `tickBehaviors(dt)` / `uninstallBehaviors(nodeId)`(签名只用 `string` / `BehaviorBinding[]` / `number`),但 `IRuntimeAdapter` 只有 `getSupportedBehaviors` + `generateBehaviorCode`。这三个运行时方法目前是 ThreeAdapter 私有契约,被 ThreeViewport 调用。A2 把它们形式化进契约。 +- **`Behavior` 接口是 Three 专属的**(`src/runtime/three/behaviors/types.ts`):`install(object: THREE.Object3D, params, ctx: BehaviorContext)`、`tick(object, params, handle, dt)`、`emit(...)`;`BehaviorContext = { scene, camera, domElement, raycaster }` 全 Three 类型。引擎中立的只有 `BehaviorDefinition`(`{ type, name, description, parameters_schema }`,在 `@/runtime/adapter`)。 +- **现有 3 个 behavior**:`auto-rotate`(无状态,`object.rotation[axis] += speed·DEG2RAD·dt`)、`bob`(per-binding handle `{base, elapsed}`,`position[axis] = base + amp·sin(2π·f·elapsed)`)、`hover-highlight`(pointer + 材质,事件驱动)。 +- **ThreeAdapter 运行时实现要点**(A2 镜像):`behaviorRuntime: Map>`;install 时 `parameters_schema.safeParse` 校验、`enabled:false` 跳过、每 binding try/catch 隔离;tick 遍历所有节点所有 binding 调 `behavior.tick?`;uninstall 调 `handle.dispose?`。 + +--- + +## 1. 目标 / 非目标 + +### 目标 + +- 把 `installBehaviors` / `tickBehaviors` / `uninstallBehaviors` **纳入 `IRuntimeAdapter`**(引擎中立签名,ThreeAdapter 已有,纯形式化 + NoopAdapter 补 stub)。 +- 新增 Babylon 侧 behavior 框架(`src/runtime/babylon/behaviors/`):`BabylonBehavior` 接口 + registry + `auto-rotate` + `bob`,共享引擎中立 `BehaviorDefinition`。 +- `BabylonAdapter` 实现三方法(镜像 ThreeAdapter 的 handle map + 错误隔离 + zod 校验)+ `getSupportedBehaviors` 返回 Babylon registry 定义。 +- conformance 套件扩展:install→tick→describeNode 断言行为运行时对两引擎成立。 + +### 非目标(各自后续) + +- **hover-highlight 跨引擎**:pointer + 材质高亮,headless 难验,本质是 B 的渲染宿主 + 事件抽象问题。延后到 B / 事件型 behavior 跨引擎时。 +- **Babylon behavior codegen**:`generateBehaviorCode` 仍抛 `not implemented`,归 A3(导出 + babylon target)。 +- **`setBehaviorDomElement` 上契约**:事件型 behavior 需要 canvas,等它们跨引擎时再加(与 hover-highlight 同批)。 +- **实时视口接 Babylon behaviors**:不碰 ThreeViewport(B)。 + +### 成功标准 + +1. `IRuntimeAdapter` 有 `installBehaviors` / `tickBehaviors` / `uninstallBehaviors`;三个实现者(ThreeAdapter / BabylonAdapter / NoopAdapter test double)都满足。 +2. conformance 双引擎全绿: + - **bob**:install + `tickBehaviors(dt)` 后 `describeNode().position[axis]` **精确等于** `base + amp·sin(2π·f·dt)`(跨引擎一致,含 handle 状态)。 + - **auto-rotate**:tick 后 rotation ≠ identity;tick 更多累积更多;`uninstallBehaviors` 后再 tick 不再变(per-engine「ran + 累积 + 可卸载」,不强求跨引擎四元数逐位相等)。 + - `enabled:false` 的 binding 不生效;未知 `behavior_type` 抛/报错(与 ThreeAdapter 对齐)。 +3. `pnpm lint && pnpm typecheck && pnpm test` 全绿。**无视觉验证**(headless)。 + +--- + +## 2. 契约改动(`src/runtime/adapter.ts`) + +在 `IRuntimeAdapter` 的 Behaviors 段加(签名与 ThreeAdapter 现有完全一致): + +```ts + // ───── Behaviors ──────────────────────────────────────────────── + getSupportedBehaviors(): BehaviorDefinition[]; + generateBehaviorCode(binding: BehaviorBinding, context: CodegenContext): string; + /** Install behavior bindings on a synced node's runtime object. Disabled + * bindings are skipped; an invalid/unknown binding is isolated (logged, not + * thrown) so one bad binding never blocks the rest. */ + installBehaviors(node_id: string, bindings: BehaviorBinding[]): void; + /** Advance every installed behavior by dt seconds. */ + tickBehaviors(dt: number): void; + /** Tear down all behaviors installed on a node (releasing per-binding state). */ + uninstallBehaviors(node_id: string): void; +``` + +- ThreeAdapter 已实现这三个 → 无改动。 +- `NoopAdapter`(`adapter.test.ts`)补三个空 stub。 +- **错误隔离语义保持与 ThreeAdapter 一致**(已核对真实代码):install 对 unknown type / invalid params(`parameters_schema.safeParse` 失败)走 `console.warn` + `continue` 跳过(**不抛**);install/tick/dispose 真抛异常时 `console.error` + 隔离(不影响其他 binding)。conformance 据此断言(见 §5)。 + +> 注:`generateBehaviorCode` 留在契约(A1 已在),Babylon 仍抛 `not implemented`(A3 才实现)。 + +## 3. Babylon behavior 框架(`src/runtime/babylon/behaviors/`) + +沿用「引擎专属代码在 `src/runtime//`」结构。Three 的 behavior 不动;Babylon 平行建。 + +### 3.1 `types.ts` + +```ts +import type { Node as BabylonNode } from "@babylonjs/core"; +import type { BehaviorDefinition } from "@/runtime/adapter"; + +/** Per-binding runtime state (bob keeps base+elapsed; auto-rotate keeps none). */ +export interface BabylonBehaviorHandle { + dispose?(): void; +} + +/** Babylon-side behavior: shares the engine-neutral BehaviorDefinition metadata + * with the Three implementation, re-implements install/tick against a Babylon + * node. No emit (codegen) in A2 — that's A3. Stateless (Flyweight): per-binding + * state lives in THandle. */ +export interface BabylonBehavior< + TParams = unknown, + THandle extends BabylonBehaviorHandle = BabylonBehaviorHandle, +> { + readonly definition: BehaviorDefinition; + install(node: BabylonNode, params: TParams): THandle; + tick?(node: BabylonNode, params: TParams, handle: THandle, dt: number): void; +} +``` + +(A2 的 behavior 不需要 scene/camera/raycaster/domElement —— auto-rotate/bob 只动节点 transform。事件型 context 等 hover-highlight 跨引擎时再引。) + +### 3.2 `registry.ts`:`BabylonBehaviorRegistry`(Map,register/get/list,镜像 `ThreeBehaviorRegistry`)。`index.ts`:`createBabylonBehaviorRegistry()` 注册 auto-rotate + bob。 + +### 3.3 `auto-rotate.ts` + +```ts +const DEG2RAD = Math.PI / 180; +// install → {}; tick → 绕 axis 累加角度。 +// Babylon 节点的 transform 在 BabylonAdapter 里用 rotationQuaternion 表达, +// 故 tick 用四元数增量:q_new = q_axisDelta · q_current。 +tick(node, params, _handle, dt) { + const tn = node as TransformNode; + const delta = Quaternion.RotationAxis(AXIS_VEC[params.axis], params.speed * DEG2RAD * dt); + const cur = tn.rotationQuaternion ?? Quaternion.Identity(); + tn.rotationQuaternion = delta.multiply(cur); +} +``` + +参数 schema 复用同一形状(axis enum + speed number)。**注意**:Babylon auto-rotate 操作 `TransformNode.rotationQuaternion`;非 TransformNode(理论上 behavior 只会装在 mesh/group 上,都是 TransformNode 子类)跳过。 + +### 3.4 `bob.ts` + +```ts +// install → { base: node.position[axis], elapsed: 0 } +// tick → node.position[axis] = base + amp * sin(2π * freq * elapsed) +install(node, params) { + const tn = node as TransformNode; + return { base: tn.position[params.axis], elapsed: 0 }; +} +tick(node, params, handle, dt) { + handle.elapsed += dt; + (node as TransformNode).position[params.axis] = + handle.base + params.amplitude * Math.sin(2 * Math.PI * params.frequency * handle.elapsed); +} +``` + +公式与 Three 的 bob **逐字一致** → describeNode 读 position 跨引擎精确相等。 + +## 4. BabylonAdapter 改动(`src/runtime/babylon/adapter.ts`) + +镜像 ThreeAdapter: + +- 字段:`private readonly behaviorRegistry = createBabylonBehaviorRegistry();` + `private readonly behaviorRuntime = new Map>();` +- `installBehaviors(nodeId, bindings)`:取 `this.objects.get(nodeId)`(无则 return);遍历 bindings,`enabled===false` 跳过;`behaviorRegistry.get(type)` 无则 `console.warn` + `continue`;`definition.parameters_schema.safeParse(params)` 失败则 `console.warn` + `continue`;`behavior.install(node, parsed.data)` 入 runtime map,单 binding `try/catch`(抛则 `console.error` 隔离)。 +- `tickBehaviors(dt)`:遍历 runtime map,每 entry `behavior.tick?.(node, params, handle, dt)`,try/catch 隔离。 +- `uninstallBehaviors(nodeId)`:取该节点 map,每 entry `handle.dispose?.()`,删 map 项。 +- `getSupportedBehaviors()`:`this.behaviorRegistry.list().map(b => b.definition)`(**取代 A1 的 `notImplemented`**)。 +- `dispose()`:额外清 `behaviorRuntime`(uninstall 全部 / clear)。 +- `generateBehaviorCode`:仍 `notImplemented`(A3)。 + +## 5. conformance 扩展(`conformance-suite.ts`,同一套跑双引擎) + +新增 `it`(用现有 `node()` helper + `make()`): + +- **bob 精确对等**: + ``` + syncNode(mesh "m" at position [0,0,0], add) + installBehaviors("m", [{ id:"b1", behavior_type:"bob", enabled:true, parameters:{axis:"y", amplitude:2, frequency:1} }]) + tickBehaviors(0.25) // sin(2π·1·0.25)=sin(π/2)=1 + expect(describeNode("m").position[1]).toBeCloseTo(0 + 2*1, 6) // = 2 + tickBehaviors(0.25) // elapsed=0.5, sin(π)=0 + expect(describeNode("m").position[1]).toBeCloseTo(0, 6) + ``` + (用 `toBeCloseTo` 容浮点;两引擎同公式 → 同值。) +- **auto-rotate ran + 累积 + 卸载**: + ``` + syncNode(mesh "r" identity, add) + installBehaviors("r", [auto-rotate axis y speed 90]) + tickBehaviors(1) + const after1 = describeNode("r").rotation + expect(after1).not.toEqual([0,0,0,1]) // 转了 + tickBehaviors(1) + expect(describeNode("r").rotation).not.toEqual(after1) // 继续转 + uninstallBehaviors("r") + const frozen = describeNode("r").rotation + tickBehaviors(1) + expect(describeNode("r").rotation).toEqual(frozen) // 卸载后冻结 + ``` +- **enabled:false 不生效**:install 一个 `enabled:false` 的 bob,tick,position 不变。 +- **未知 type 隔离不抛**:`installBehaviors("m", [{behavior_type:"nope",...}])` 不抛(与 ThreeAdapter 隔离语义一致);可配合一个有效 binding 验"坏的被跳过、好的仍生效"。 + +> conformance 现在断言 `getSupportedBehaviors()` 至少含 auto-rotate + bob(两引擎都注册了这俩)。 + +## 6. 边界 / 错误处理 + +- behavior 装在未 syncNode 的 nodeId:`installBehaviors` 取不到对象 → return(no-op),与 ThreeAdapter 一致。 +- 未知 behavior_type / 非法 params:`console.error` 隔离,不抛、不影响其他 binding。 +- 一个 binding 的 install/tick 抛:try/catch 隔离,记 `console.error`。 +- `tickBehaviors` 在无任何安装时:no-op。 +- Babylon 非 TransformNode 节点(A2 behavior 只会装 mesh/group,均 TransformNode 子类):理论不触发;防御性 cast,真遇到非 TransformNode 则该 behavior tick 跳过(不抛)。 + +## 7. 测试策略 + +- **conformance**(双引擎,§5):bob 精确对等是核心;auto-rotate「ran/累积/卸载」;enabled/未知隔离。 +- **Babylon behaviors 单测**(`babylon/behaviors/*.test.ts`):auto-rotate tick 改 quaternion;bob tick = 公式;registry register/get/list + 重复抛。 +- **BabylonAdapter 单测追加**:installBehaviors + tickBehaviors 改 describeNode;uninstall 冻结;getSupportedBehaviors 含俩;未知 type 不抛。 +- 无视觉(headless)。`pnpm lint && pnpm typecheck && pnpm test` 全绿。 + +## 8. 文件清单 + +**新增**: + +- `src/runtime/babylon/behaviors/types.ts` +- `src/runtime/babylon/behaviors/registry.ts`(+ `registry.test.ts`) +- `src/runtime/babylon/behaviors/auto-rotate.ts`(+ `auto-rotate.test.ts`) +- `src/runtime/babylon/behaviors/bob.ts`(+ `bob.test.ts`) +- `src/runtime/babylon/behaviors/index.ts` + +**修改**: + +- `src/runtime/adapter.ts`(契约加 installBehaviors / tickBehaviors / uninstallBehaviors) +- `src/runtime/adapter.test.ts`(NoopAdapter 补三 stub) +- `src/runtime/babylon/adapter.ts`(实现三方法 + behaviorRegistry/Runtime + getSupportedBehaviors + dispose 清理)+ `adapter.test.ts`(追加) +- `src/runtime/conformance/conformance-suite.ts`(+ behavior 断言) + +**不改**:`ThreeViewport.tsx`、Three behaviors、导出/codegen(A3)。 + +## 9. 风险 / 延后 + +- **auto-rotate 跨引擎不逐位对等**:已知、记录(同 A1 的 rotation 语义 gap)。bob 给精确对等兜底,证明运行时管线 + handle 状态跨引擎一致。逐位旋转对等若未来需要,归 B/朝向统一。 +- hover-highlight / 事件型 behavior 跨引擎、Babylon behavior codegen、setBehaviorDomElement 上契约 → 后续(B / A3)。 + +## 10. 反推源(plan 阶段确认的符号) + +- `src/runtime/three/adapter.ts`:`installBehaviors`/`tickBehaviors`/`uninstallBehaviors`/`behaviorRuntime` 现状(行号 + 错误隔离写法),照镜像。 +- `src/runtime/three/behaviors/{registry,types,auto-rotate,bob}.ts`:registry/接口/两 behavior 的参数 schema + 公式,Babylon 复刻。 +- `src/runtime/babylon/adapter.ts`:A1 现状(objects map、metadata、dispose、notImplemented),在其上加 behavior。 +- `@babylonjs/core` 9.x:`Quaternion.RotationAxis` / `Quaternion.Identity` / `Quaternion.multiply` / `TransformNode.rotationQuaternion` / `Vector3` 索引(`v[axis]`)的具名 API。 +- `src/runtime/conformance/conformance-suite.ts`:`node()` helper / `make()` / `afterEach` dispose 模式,behavior 断言并入。 +- `src/runtime/adapter.test.ts`:NoopAdapter 现有方法,补 stub。 + +## 11. 验收 + +满足 §1 成功标准:契约含三方法、双引擎实现、conformance bob 精确对等 + auto-rotate ran/累积/卸载 + enabled/未知隔离全绿、`lint/typecheck/test` 全绿。下一步:A3(glTF 加载 + babylon 导出 target,含 behavior codegen),或 B(实时视口切引擎)。 From 63cd583607a11dd57e9f90c47e900f449cf01989 Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Wed, 10 Jun 2026 12:14:42 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(babylon):=20behavior=20framework=20?= =?UTF-8?q?=E2=80=94=20registry=20+=20auto-rotate=20+=20bob?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../babylon/behaviors/auto-rotate.test.ts | 24 +++++++++ src/runtime/babylon/behaviors/auto-rotate.ts | 49 +++++++++++++++++++ src/runtime/babylon/behaviors/bob.test.ts | 26 ++++++++++ src/runtime/babylon/behaviors/bob.ts | 42 ++++++++++++++++ src/runtime/babylon/behaviors/index.ts | 15 ++++++ .../babylon/behaviors/registry.test.ts | 41 ++++++++++++++++ src/runtime/babylon/behaviors/registry.ts | 23 +++++++++ src/runtime/babylon/behaviors/types.ts | 23 +++++++++ 8 files changed, 243 insertions(+) create mode 100644 src/runtime/babylon/behaviors/auto-rotate.test.ts create mode 100644 src/runtime/babylon/behaviors/auto-rotate.ts create mode 100644 src/runtime/babylon/behaviors/bob.test.ts create mode 100644 src/runtime/babylon/behaviors/bob.ts create mode 100644 src/runtime/babylon/behaviors/index.ts create mode 100644 src/runtime/babylon/behaviors/registry.test.ts create mode 100644 src/runtime/babylon/behaviors/registry.ts create mode 100644 src/runtime/babylon/behaviors/types.ts diff --git a/src/runtime/babylon/behaviors/auto-rotate.test.ts b/src/runtime/babylon/behaviors/auto-rotate.test.ts new file mode 100644 index 0000000..da807bd --- /dev/null +++ b/src/runtime/babylon/behaviors/auto-rotate.test.ts @@ -0,0 +1,24 @@ +import { NullEngine, Quaternion, Scene, TransformNode } from "@babylonjs/core"; +import { describe, expect, it } from "vitest"; + +import { AutoRotateBehavior } from "./auto-rotate"; + +describe("Babylon AutoRotateBehavior", () => { + it("has the auto-rotate definition", () => { + expect(new AutoRotateBehavior().definition.type).toBe("auto-rotate"); + }); + + it("tick rotates the node's quaternion around the axis", () => { + const engine = new NullEngine(); + const scene = new Scene(engine); + const node = new TransformNode("n", scene); + node.rotationQuaternion = Quaternion.Identity(); + const b = new AutoRotateBehavior(); + const handle = b.install(node, { axis: "y", speed: 90 }); + b.tick!(node, { axis: "y", speed: 90 }, handle, 1); // 90°/s · 1s = 90° + expect(node.rotationQuaternion!.y).toBeCloseTo(Math.SQRT1_2, 5); + expect(node.rotationQuaternion!.w).toBeCloseTo(Math.SQRT1_2, 5); + scene.dispose(); + engine.dispose(); + }); +}); diff --git a/src/runtime/babylon/behaviors/auto-rotate.ts b/src/runtime/babylon/behaviors/auto-rotate.ts new file mode 100644 index 0000000..0088d60 --- /dev/null +++ b/src/runtime/babylon/behaviors/auto-rotate.ts @@ -0,0 +1,49 @@ +import { + Quaternion, + type Node as BabylonNode, + TransformNode, + Vector3, +} from "@babylonjs/core"; +import { z } from "zod"; + +import type { BehaviorDefinition } from "@/runtime/adapter"; + +import type { BabylonBehavior, BabylonBehaviorHandle } from "./types"; + +const ParamsSchema = z.object({ axis: z.enum(["x", "y", "z"]), speed: z.number() }); +type Params = z.infer; + +const DEG2RAD = Math.PI / 180; +const AXIS_VEC: Record = { + x: new Vector3(1, 0, 0), + y: new Vector3(0, 1, 0), + z: new Vector3(0, 0, 1), +}; + +export class AutoRotateBehavior implements BabylonBehavior { + readonly definition: BehaviorDefinition = { + type: "auto-rotate", + name: "Auto Rotate", + description: "Rotates the node around a local axis at a constant angular velocity.", + parameters_schema: ParamsSchema, + }; + + install(_node: BabylonNode, _params: Params): BabylonBehaviorHandle { + return {}; + } + + tick( + node: BabylonNode, + params: Params, + _handle: BabylonBehaviorHandle, + dt: number, + ): void { + if (!(node instanceof TransformNode)) return; + const delta = Quaternion.RotationAxis( + AXIS_VEC[params.axis], + params.speed * DEG2RAD * dt, + ); + const cur = node.rotationQuaternion ?? Quaternion.Identity(); + node.rotationQuaternion = delta.multiply(cur); + } +} diff --git a/src/runtime/babylon/behaviors/bob.test.ts b/src/runtime/babylon/behaviors/bob.test.ts new file mode 100644 index 0000000..46f7714 --- /dev/null +++ b/src/runtime/babylon/behaviors/bob.test.ts @@ -0,0 +1,26 @@ +import { NullEngine, Scene, TransformNode, Vector3 } from "@babylonjs/core"; +import { describe, expect, it } from "vitest"; + +import { BobBehavior } from "./bob"; + +describe("Babylon BobBehavior", () => { + it("has the bob definition", () => { + expect(new BobBehavior().definition.type).toBe("bob"); + }); + + it("tick sets position to base + amp*sin(2π f t)", () => { + const engine = new NullEngine(); + const scene = new Scene(engine); + const node = new TransformNode("n", scene); + node.position = new Vector3(0, 0, 0); + const params = { axis: "y" as const, amplitude: 2, frequency: 1 }; + const b = new BobBehavior(); + const handle = b.install(node, params); + b.tick!(node, params, handle, 0.25); // sin(π/2)=1 → y=2 + expect(node.position.y).toBeCloseTo(2, 6); + b.tick!(node, params, handle, 0.25); // elapsed 0.5, sin(π)=0 → y=0 + expect(node.position.y).toBeCloseTo(0, 6); + scene.dispose(); + engine.dispose(); + }); +}); diff --git a/src/runtime/babylon/behaviors/bob.ts b/src/runtime/babylon/behaviors/bob.ts new file mode 100644 index 0000000..1dd1629 --- /dev/null +++ b/src/runtime/babylon/behaviors/bob.ts @@ -0,0 +1,42 @@ +import { type Node as BabylonNode, TransformNode } from "@babylonjs/core"; +import { z } from "zod"; + +import type { BehaviorDefinition } from "@/runtime/adapter"; + +import type { BabylonBehavior, BabylonBehaviorHandle } from "./types"; + +const ParamsSchema = z.object({ + axis: z.enum(["x", "y", "z"]), + amplitude: z.number(), + frequency: z.number(), +}); +type Params = z.infer; + +interface BobHandle extends BabylonBehaviorHandle { + base: number; + elapsed: number; +} + +const TWO_PI = Math.PI * 2; + +export class BobBehavior implements BabylonBehavior { + readonly definition: BehaviorDefinition = { + type: "bob", + name: "Bob", + description: "Floats the node up and down along a local axis (sine wave).", + parameters_schema: ParamsSchema, + }; + + install(node: BabylonNode, params: Params): BobHandle { + const base = node instanceof TransformNode ? node.position[params.axis] : 0; + return { base, elapsed: 0 }; + } + + tick(node: BabylonNode, params: Params, handle: BobHandle, dt: number): void { + if (!(node instanceof TransformNode)) return; + handle.elapsed += dt; + node.position[params.axis] = + handle.base + + params.amplitude * Math.sin(TWO_PI * params.frequency * handle.elapsed); + } +} diff --git a/src/runtime/babylon/behaviors/index.ts b/src/runtime/babylon/behaviors/index.ts new file mode 100644 index 0000000..dc30d49 --- /dev/null +++ b/src/runtime/babylon/behaviors/index.ts @@ -0,0 +1,15 @@ +import { AutoRotateBehavior } from "./auto-rotate"; +import { BobBehavior } from "./bob"; +import { BabylonBehaviorRegistry } from "./registry"; + +export { BabylonBehaviorRegistry } from "./registry"; +export type { BabylonBehavior, BabylonBehaviorHandle } from "./types"; + +/** Build a registry pre-populated with the A2 behavior catalog (auto-rotate + + * bob). Per-adapter instance, mirroring createThreeBehaviorRegistry. */ +export function createBabylonBehaviorRegistry(): BabylonBehaviorRegistry { + const r = new BabylonBehaviorRegistry(); + r.register(new AutoRotateBehavior()); + r.register(new BobBehavior()); + return r; +} diff --git a/src/runtime/babylon/behaviors/registry.test.ts b/src/runtime/babylon/behaviors/registry.test.ts new file mode 100644 index 0000000..5236012 --- /dev/null +++ b/src/runtime/babylon/behaviors/registry.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +import type { BabylonBehavior, BabylonBehaviorHandle } from "./types"; +import { BabylonBehaviorRegistry } from "./registry"; + +function fakeBehavior(type: string): BabylonBehavior { + return { + definition: { type, name: type, description: "", parameters_schema: z.object({}) }, + install: (): BabylonBehaviorHandle => ({}), + tick: () => {}, + }; +} + +describe("BabylonBehaviorRegistry", () => { + it("registers and retrieves by type", () => { + const r = new BabylonBehaviorRegistry(); + const b = fakeBehavior("alpha"); + r.register(b); + expect(r.get("alpha")).toBe(b); + }); + + it("returns undefined for unregistered types", () => { + expect(new BabylonBehaviorRegistry().get("nope")).toBeUndefined(); + }); + + it("list() returns every registered behavior in insertion order", () => { + const r = new BabylonBehaviorRegistry(); + const a = fakeBehavior("a"); + const b = fakeBehavior("b"); + r.register(a); + r.register(b); + expect(r.list()).toEqual([a, b]); + }); + + it("throws on duplicate type", () => { + const r = new BabylonBehaviorRegistry(); + r.register(fakeBehavior("dup")); + expect(() => r.register(fakeBehavior("dup"))).toThrow(/duplicate type "dup"/); + }); +}); diff --git a/src/runtime/babylon/behaviors/registry.ts b/src/runtime/babylon/behaviors/registry.ts new file mode 100644 index 0000000..378307d --- /dev/null +++ b/src/runtime/babylon/behaviors/registry.ts @@ -0,0 +1,23 @@ +import type { BabylonBehavior } from "./types"; + +/** Per-adapter registry of Babylon behavior implementations, keyed by + * BehaviorDefinition.type. Mirrors ThreeBehaviorRegistry. */ +export class BabylonBehaviorRegistry { + private readonly behaviors = new Map(); + + register(b: BabylonBehavior): void { + const type = b.definition.type; + if (this.behaviors.has(type)) { + throw new Error(`BabylonBehaviorRegistry: duplicate type "${type}"`); + } + this.behaviors.set(type, b); + } + + get(type: string): BabylonBehavior | undefined { + return this.behaviors.get(type); + } + + list(): BabylonBehavior[] { + return [...this.behaviors.values()]; + } +} diff --git a/src/runtime/babylon/behaviors/types.ts b/src/runtime/babylon/behaviors/types.ts new file mode 100644 index 0000000..a37fe98 --- /dev/null +++ b/src/runtime/babylon/behaviors/types.ts @@ -0,0 +1,23 @@ +import type { Node as BabylonNode } from "@babylonjs/core"; + +import type { BehaviorDefinition } from "@/runtime/adapter"; + +/** Per-binding runtime state (bob keeps base+elapsed; auto-rotate keeps none). */ +export interface BabylonBehaviorHandle { + dispose?(): void; +} + +/** + * Babylon-side behavior: shares the engine-neutral BehaviorDefinition metadata + * with the Three implementation, re-implements install/tick against a Babylon + * node. No emit (codegen) in A2 — that's A3. Stateless (Flyweight): per-binding + * state lives in THandle so one instance serves many bindings. + */ +export interface BabylonBehavior< + TParams = unknown, + THandle extends BabylonBehaviorHandle = BabylonBehaviorHandle, +> { + readonly definition: BehaviorDefinition; + install(node: BabylonNode, params: TParams): THandle; + tick?(node: BabylonNode, params: TParams, handle: THandle, dt: number): void; +} From 2682d2270565b7299eafbf193c5f1893e401eb1d Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Wed, 10 Jun 2026 12:18:23 +0800 Subject: [PATCH 3/7] feat(adapter): behavior runtime on contract; BabylonAdapter install/tick/uninstall --- src/runtime/adapter.test.ts | 4 ++ src/runtime/adapter.ts | 9 ++++ src/runtime/babylon/adapter.test.ts | 78 ++++++++++++++++++++++++++++ src/runtime/babylon/adapter.ts | 79 ++++++++++++++++++++++++++++- 4 files changed, 169 insertions(+), 1 deletion(-) diff --git a/src/runtime/adapter.test.ts b/src/runtime/adapter.test.ts index af4be46..b71dd4c 100644 --- a/src/runtime/adapter.test.ts +++ b/src/runtime/adapter.test.ts @@ -51,6 +51,10 @@ class NoopAdapter implements IRuntimeAdapter { dispose(): void {} + installBehaviors(_node_id: string, _bindings: BehaviorBinding[]): void {} + tickBehaviors(_dt: number): void {} + uninstallBehaviors(_node_id: string): void {} + getSupportedBehaviors(): BehaviorDefinition[] { return [ { diff --git a/src/runtime/adapter.ts b/src/runtime/adapter.ts index 08fde3b..f8895cd 100644 --- a/src/runtime/adapter.ts +++ b/src/runtime/adapter.ts @@ -159,6 +159,15 @@ export interface IRuntimeAdapter { getSupportedBehaviors(): BehaviorDefinition[]; generateBehaviorCode(binding: BehaviorBinding, context: CodegenContext): string; + /** Install behavior bindings on a synced node's runtime object. Disabled + * bindings are skipped; an invalid/unknown binding is isolated (logged via + * console.warn, not thrown) so one bad binding never blocks the rest. */ + installBehaviors(node_id: string, bindings: BehaviorBinding[]): void; + /** Advance every installed behavior by dt seconds. */ + tickBehaviors(dt: number): void; + /** Tear down all behaviors installed on a node (releasing per-binding state). */ + uninstallBehaviors(node_id: string): void; + // ───── Lifecycle ──────────────────────────────────────────────── /** Release all engine handles (renderer/scene/GPU resources). Both shipped * adapters already implement this; the viewport calls it on teardown. */ diff --git a/src/runtime/babylon/adapter.test.ts b/src/runtime/babylon/adapter.test.ts index e6d9079..7c34a04 100644 --- a/src/runtime/babylon/adapter.test.ts +++ b/src/runtime/babylon/adapter.test.ts @@ -122,3 +122,81 @@ describe("BabylonAdapter", () => { a.dispose(); }); }); + +describe("BabylonAdapter behaviors", () => { + function meshAt(id: string, y: number): SceneNode { + return { + id, + name: id, + type: "mesh", + parent_id: null, + transform: { position: [0, y, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] }, + children_ids: [], + visible: true, + locked: false, + data: { type: "mesh", geometry: { kind: "box" } }, + behaviors: [], + user_data: {}, + }; + } + + it("getSupportedBehaviors returns auto-rotate + bob", () => { + const types = new BabylonAdapter().getSupportedBehaviors().map((d) => d.type); + expect(types).toContain("auto-rotate"); + expect(types).toContain("bob"); + }); + + it("install + tick bob moves the node by the sine formula", () => { + const a = new BabylonAdapter(); + a.syncNode(meshAt("m", 0), "add"); + a.installBehaviors("m", [ + { + id: "b1", + behavior_type: "bob", + enabled: true, + parameters: { axis: "y", amplitude: 2, frequency: 1 }, + }, + ]); + a.tickBehaviors(0.25); + expect(a.describeNode("m")!.position[1]).toBeCloseTo(2, 6); + a.dispose(); + }); + + it("uninstall freezes the behavior", () => { + const a = new BabylonAdapter(); + a.syncNode(meshAt("m", 0), "add"); + a.installBehaviors("m", [ + { + id: "b1", + behavior_type: "bob", + enabled: true, + parameters: { axis: "y", amplitude: 2, frequency: 1 }, + }, + ]); + a.tickBehaviors(0.25); + a.uninstallBehaviors("m"); + const frozen = a.describeNode("m")!.position[1]; + a.tickBehaviors(0.25); + expect(a.describeNode("m")!.position[1]).toBe(frozen); + a.dispose(); + }); + + it("skips disabled bindings and isolates unknown types (no throw)", () => { + const a = new BabylonAdapter(); + a.syncNode(meshAt("m", 0), "add"); + expect(() => + a.installBehaviors("m", [ + { id: "x", behavior_type: "nope", enabled: true, parameters: {} }, + { + id: "d", + behavior_type: "bob", + enabled: false, + parameters: { axis: "y", amplitude: 2, frequency: 1 }, + }, + ]), + ).not.toThrow(); + a.tickBehaviors(0.25); + expect(a.describeNode("m")!.position[1]).toBe(0); + a.dispose(); + }); +}); diff --git a/src/runtime/babylon/adapter.ts b/src/runtime/babylon/adapter.ts index 12cec8d..d8493f0 100644 --- a/src/runtime/babylon/adapter.ts +++ b/src/runtime/babylon/adapter.ts @@ -31,6 +31,12 @@ import type { RuntimeNodeInfo, SyncOp, } from "../adapter"; +import { + createBabylonBehaviorRegistry, + type BabylonBehavior, + type BabylonBehaviorHandle, + type BabylonBehaviorRegistry, +} from "./behaviors"; const BABYLON_TARGET: RuntimeTarget = { kind: "babylon.js", version: "9.11.0" }; @@ -58,6 +64,15 @@ export class BabylonAdapter implements IRuntimeAdapter { private readonly engine = new NullEngine(); private readonly scene = new Scene(this.engine); private readonly objects = new Map(); + private readonly behaviorRegistry: BabylonBehaviorRegistry = + createBabylonBehaviorRegistry(); + private readonly behaviorRuntime = new Map< + string, + Map< + string, + { behavior: BabylonBehavior; params: unknown; handle: BabylonBehaviorHandle } + > + >(); syncNode(node: SceneNode, op: SyncOp): void { if (op === "remove") { @@ -170,6 +185,7 @@ export class BabylonAdapter implements IRuntimeAdapter { this.scene.dispose(); this.engine.dispose(); this.objects.clear(); + this.behaviorRuntime.clear(); } pickAt(_screen_x: number, _screen_y: number): string | null { @@ -189,8 +205,69 @@ export class BabylonAdapter implements IRuntimeAdapter { ); } getSupportedBehaviors(): BehaviorDefinition[] { - return notImplemented("getSupportedBehaviors"); + return this.behaviorRegistry.list().map((b) => b.definition); + } + + installBehaviors(node_id: string, bindings: BehaviorBinding[]): void { + const node = this.objects.get(node_id); + if (!node) return; + const perNode = new Map< + string, + { behavior: BabylonBehavior; params: unknown; handle: BabylonBehaviorHandle } + >(); + for (const binding of bindings) { + if (!binding.enabled) continue; + const b = this.behaviorRegistry.get(binding.behavior_type); + if (!b) { + console.warn( + `installBehaviors: unknown behavior_type "${binding.behavior_type}"`, + ); + continue; + } + const parsed = b.definition.parameters_schema.safeParse(binding.parameters); + if (!parsed.success) { + console.warn( + `installBehaviors: invalid params on binding ${binding.id} (${binding.behavior_type})`, + ); + continue; + } + try { + const handle = b.install(node, parsed.data); + perNode.set(binding.id, { behavior: b, params: parsed.data, handle }); + } catch (e) { + console.error(`installBehaviors: install threw on ${binding.id}`, e); + } + } + this.behaviorRuntime.set(node_id, perNode); } + + tickBehaviors(dt: number): void { + for (const [node_id, perNode] of this.behaviorRuntime) { + const node = this.objects.get(node_id); + if (!node) continue; + for (const entry of perNode.values()) { + try { + entry.behavior.tick?.(node, entry.params, entry.handle, dt); + } catch (e) { + console.error(`tickBehaviors: tick threw on node ${node_id}`, e); + } + } + } + } + + uninstallBehaviors(node_id: string): void { + const perNode = this.behaviorRuntime.get(node_id); + if (!perNode) return; + for (const entry of perNode.values()) { + try { + entry.handle.dispose?.(); + } catch (e) { + console.error("uninstallBehaviors: dispose threw", e); + } + } + this.behaviorRuntime.delete(node_id); + } + generateBehaviorCode(_binding: BehaviorBinding, _context: CodegenContext): string { return notImplemented("generateBehaviorCode"); } From 3ba5b807f5c3314bd4c4238451bf9a226fb78ac9 Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Wed, 10 Jun 2026 12:20:51 +0800 Subject: [PATCH 4/7] =?UTF-8?q?test(conformance):=20behavior=20runtime=20p?= =?UTF-8?q?arity=20=E2=80=94=20bob=20exact,=20auto-rotate=20ran=20(both=20?= =?UTF-8?q?engines)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/runtime/conformance/conformance-suite.ts | 89 ++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/runtime/conformance/conformance-suite.ts b/src/runtime/conformance/conformance-suite.ts index b3487de..cace387 100644 --- a/src/runtime/conformance/conformance-suite.ts +++ b/src/runtime/conformance/conformance-suite.ts @@ -240,5 +240,94 @@ export function describeAdapterConformance( ), ).toThrow(); }); + + it("getSupportedBehaviors includes auto-rotate + bob", () => { + const types = make() + .getSupportedBehaviors() + .map((d) => d.type); + expect(types).toContain("auto-rotate"); + expect(types).toContain("bob"); + }); + + it("bob behavior moves the node by base + amp*sin(2π f t) (exact across engines)", () => { + const a = make(); + a.syncNode( + node({ + id: "m", + name: "m", + type: "mesh", + data: { type: "mesh", geometry: { kind: "box" } }, + }), + "add", + ); + a.installBehaviors("m", [ + { + id: "b1", + behavior_type: "bob", + enabled: true, + parameters: { axis: "y", amplitude: 2, frequency: 1 }, + }, + ]); + a.tickBehaviors(0.25); // sin(π/2)=1 → y=2 + expect(a.describeNode("m")!.position[1]).toBeCloseTo(2, 6); + a.tickBehaviors(0.25); // sin(π)=0 → y=0 + expect(a.describeNode("m")!.position[1]).toBeCloseTo(0, 6); + }); + + it("auto-rotate behavior runs, accumulates, and freezes on uninstall", () => { + const a = make(); + a.syncNode( + node({ + id: "r", + name: "r", + type: "mesh", + data: { type: "mesh", geometry: { kind: "box" } }, + }), + "add", + ); + a.installBehaviors("r", [ + { + id: "b1", + behavior_type: "auto-rotate", + enabled: true, + parameters: { axis: "y", speed: 90 }, + }, + ]); + a.tickBehaviors(1); + const after1 = a.describeNode("r")!.rotation; + expect(after1).not.toEqual([0, 0, 0, 1]); // it rotated + a.tickBehaviors(1); + expect(a.describeNode("r")!.rotation).not.toEqual(after1); // it kept rotating + a.uninstallBehaviors("r"); + const frozen = a.describeNode("r")!.rotation; + a.tickBehaviors(1); + expect(a.describeNode("r")!.rotation).toEqual(frozen); // frozen after uninstall + }); + + it("skips disabled bindings and isolates unknown behavior types (no throw)", () => { + const a = make(); + a.syncNode( + node({ + id: "m", + name: "m", + type: "mesh", + data: { type: "mesh", geometry: { kind: "box" } }, + }), + "add", + ); + expect(() => + a.installBehaviors("m", [ + { id: "x", behavior_type: "nope", enabled: true, parameters: {} }, + { + id: "d", + behavior_type: "bob", + enabled: false, + parameters: { axis: "y", amplitude: 2, frequency: 1 }, + }, + ]), + ).not.toThrow(); + a.tickBehaviors(0.25); + expect(a.describeNode("m")!.position[1]).toBe(0); // disabled bob didn't move it + }); }); } From 3698fc93094930c224aa5270cc6a9dcc47d6e619 Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Wed, 10 Jun 2026 12:27:37 +0800 Subject: [PATCH 5/7] docs(adapter): clarify installBehaviors isolation (warn-skip vs catch-on-throw) Co-Authored-By: Claude Opus 4.8 --- src/runtime/adapter.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/runtime/adapter.ts b/src/runtime/adapter.ts index f8895cd..562ee58 100644 --- a/src/runtime/adapter.ts +++ b/src/runtime/adapter.ts @@ -160,8 +160,9 @@ export interface IRuntimeAdapter { generateBehaviorCode(binding: BehaviorBinding, context: CodegenContext): string; /** Install behavior bindings on a synced node's runtime object. Disabled - * bindings are skipped; an invalid/unknown binding is isolated (logged via - * console.warn, not thrown) so one bad binding never blocks the rest. */ + * bindings are skipped; unknown/invalid ones are skipped with a warning; a + * binding whose install throws is caught and logged. No bad binding ever + * blocks the rest (all failures are isolated, never re-thrown). */ installBehaviors(node_id: string, bindings: BehaviorBinding[]): void; /** Advance every installed behavior by dt seconds. */ tickBehaviors(dt: number): void; From 0ac04a3a63a71b6453875a7dc659801e2040f055 Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Wed, 10 Jun 2026 12:29:03 +0800 Subject: [PATCH 6/7] =?UTF-8?q?docs(plan):=20v1.0=20sub-stage=20A2=20?= =?UTF-8?q?=E2=80=94=20Babylon=20behaviors=20=E5=AE=9E=E6=96=BD=E8=AE=A1?= =?UTF-8?q?=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- ...026-06-10-v1.0a2-babylon-behaviors-plan.md | 776 ++++++++++++++++++ 1 file changed, 776 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-10-v1.0a2-babylon-behaviors-plan.md diff --git a/docs/superpowers/plans/2026-06-10-v1.0a2-babylon-behaviors-plan.md b/docs/superpowers/plans/2026-06-10-v1.0a2-babylon-behaviors-plan.md new file mode 100644 index 0000000..a8e51dc --- /dev/null +++ b/docs/superpowers/plans/2026-06-10-v1.0a2-babylon-behaviors-plan.md @@ -0,0 +1,776 @@ +# v1.0 sub-stage A2 · Babylon behaviors 跨引擎运行时 实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 把 behavior 运行时三方法(`installBehaviors`/`tickBehaviors`/`uninstallBehaviors`)纳入 `IRuntimeAdapter`;给 Babylon 写 auto-rotate + bob 实现;conformance 验两引擎运行时对等(bob 精确、auto-rotate ran/累积/卸载)。 + +**架构:** Babylon 平行建 `src/runtime/babylon/behaviors/`(自己的 registry + 2 behavior,操作 Babylon 节点,只共享引擎中立 `BehaviorDefinition`)。`BabylonAdapter` 镜像 ThreeAdapter 的 `behaviorRuntime` map + 错误隔离实现三方法。conformance 套件加 behavior 断言,跑双引擎。纯 headless。 + +**技术栈:** TypeScript + `@babylonjs/core` 9.x(`NullEngine`/`Quaternion`/`TransformNode`)+ vitest/jsdom + zod。 + +--- + +## 关键约束与发现(实施前已读真实代码) + +1. **错误隔离语义(已核对 `three/adapter.ts:417-481`)**:`installBehaviors` 对 `!enabled` → `continue`;unknown type → `console.warn` + `continue`;`safeParse` 失败 → `console.warn` + `continue`;`install` 抛 → `console.error` 隔离。`tickBehaviors` 每 entry try/catch(`console.error`)。`uninstallBehaviors` 每 handle `dispose?()` try/catch。Babylon **逐条镜像**。conformance 的"未知 type 不抛"据此成立。 +2. **BabylonAdapter 现状**(`babylon/adapter.ts`):`notImplemented(method)` helper(:45);`private readonly objects = new Map()`(:60);`dispose()`(:169,清 scene/engine/objects——A2 加清 behaviorRuntime);`getSupportedBehaviors`(:191,现 `notImplemented` → A2 替换);`generateBehaviorCode`(:195,**仍 `notImplemented`**,A3 才做)。 +3. **registry 模式**(`three/behaviors/registry.ts` + `index.ts`):`Map`,`register`(重复抛 `duplicate type "..."`)/`get`/`list`;`create…Registry()` = new + 逐个 `register(new X())`。Babylon 逐字镜像。 +4. **NoopAdapter**(`adapter.test.ts:26`):已有 dispose/getSupportedBehaviors/generateBehaviorCode;A2 补 install/tick/uninstall 三个空 stub。`adapter.test.ts:90` 用 `getSupportedBehaviors()[0]`,故 Noop 仍返回非空。 +5. **Babylon `Vector3` 索引**:`tn.position[axis]`(axis: "x"|"y"|"z")TS 上是 number(Vector3 有 x/y/z);若 TS 报索引错,`position[axis as "x"|"y"|"z"]`。 +6. **conformance 用现成脚手架**:`conformance-suite.ts` 的 `node()` helper、`make()`(追踪 + afterEach dispose)、`describeAdapterConformance(makeAdapter,label)`——behavior 断言并入同一 describe。 +7. **执行前提**:分支 `feat/v1.0a2-babylon-behaviors`(spec `474591c` 已在)。`pnpm`/`git`(Node 20);commit 触发 husky+lint-staged。 + +--- + +## 文件结构 + +**新增** + +| 文件 | 职责 | +| ------------------------------------------------------------- | ------------------------------------------------ | +| `src/runtime/babylon/behaviors/types.ts` | `BabylonBehavior` + `BabylonBehaviorHandle` 接口 | +| `src/runtime/babylon/behaviors/registry.ts` (+ `.test.ts`) | `BabylonBehaviorRegistry`(镜像 Three) | +| `src/runtime/babylon/behaviors/auto-rotate.ts` (+ `.test.ts`) | Babylon auto-rotate(四元数增量) | +| `src/runtime/babylon/behaviors/bob.ts` (+ `.test.ts`) | Babylon bob(公式与 Three 逐字一致) | +| `src/runtime/babylon/behaviors/index.ts` | `createBabylonBehaviorRegistry()` | + +**修改** + +| 文件 | 改动 | +| ---------------------------------------------- | ------------------------------------------------------------------------------------- | +| `src/runtime/adapter.ts` | 契约加 `installBehaviors`/`tickBehaviors`/`uninstallBehaviors` | +| `src/runtime/adapter.test.ts` | NoopAdapter 补三 stub | +| `src/runtime/babylon/adapter.ts` | behaviorRegistry/Runtime 字段 + 三方法实现 + `getSupportedBehaviors` + `dispose` 清理 | +| `src/runtime/babylon/adapter.test.ts` | behavior 单测追加 | +| `src/runtime/conformance/conformance-suite.ts` | behavior 断言(双引擎) | + +依赖顺序:1(Babylon behaviors leaf,standalone)→ 2(契约 + NoopAdapter + BabylonAdapter 一起,保持绿)→ 3(conformance)→ 4(验证收尾)。 + +--- + +### 任务 1:Babylon behavior 框架(types + registry + auto-rotate + bob + index) + +**文件:** 新建 `src/runtime/babylon/behaviors/` 下 5 个源文件 + 3 个测试。不碰契约/adapter,完全 standalone。 + +- [ ] **步骤 1:types.ts** + +创建 `src/runtime/babylon/behaviors/types.ts`: + +```ts +import type { Node as BabylonNode } from "@babylonjs/core"; + +import type { BehaviorDefinition } from "@/runtime/adapter"; + +/** Per-binding runtime state (bob keeps base+elapsed; auto-rotate keeps none). */ +export interface BabylonBehaviorHandle { + dispose?(): void; +} + +/** + * Babylon-side behavior: shares the engine-neutral BehaviorDefinition metadata + * with the Three implementation, re-implements install/tick against a Babylon + * node. No emit (codegen) in A2 — that's A3. Stateless (Flyweight): per-binding + * state lives in THandle so one instance serves many bindings. + */ +export interface BabylonBehavior< + TParams = unknown, + THandle extends BabylonBehaviorHandle = BabylonBehaviorHandle, +> { + readonly definition: BehaviorDefinition; + install(node: BabylonNode, params: TParams): THandle; + tick?(node: BabylonNode, params: TParams, handle: THandle, dt: number): void; +} +``` + +- [ ] **步骤 2:registry — 先写失败测试** + +创建 `src/runtime/babylon/behaviors/registry.test.ts`(镜像 three 的 registry.test): + +```ts +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +import type { BabylonBehavior, BabylonBehaviorHandle } from "./types"; +import { BabylonBehaviorRegistry } from "./registry"; + +function fakeBehavior(type: string): BabylonBehavior { + return { + definition: { type, name: type, description: "", parameters_schema: z.object({}) }, + install: (): BabylonBehaviorHandle => ({}), + tick: () => {}, + }; +} + +describe("BabylonBehaviorRegistry", () => { + it("registers and retrieves by type", () => { + const r = new BabylonBehaviorRegistry(); + const b = fakeBehavior("alpha"); + r.register(b); + expect(r.get("alpha")).toBe(b); + }); + + it("returns undefined for unregistered types", () => { + expect(new BabylonBehaviorRegistry().get("nope")).toBeUndefined(); + }); + + it("list() returns every registered behavior in insertion order", () => { + const r = new BabylonBehaviorRegistry(); + const a = fakeBehavior("a"); + const b = fakeBehavior("b"); + r.register(a); + r.register(b); + expect(r.list()).toEqual([a, b]); + }); + + it("throws on duplicate type", () => { + const r = new BabylonBehaviorRegistry(); + r.register(fakeBehavior("dup")); + expect(() => r.register(fakeBehavior("dup"))).toThrow(/duplicate type "dup"/); + }); +}); +``` + +运行 `pnpm vitest run src/runtime/babylon/behaviors/registry.test.ts` → FAIL(模块不存在)。 + +- [ ] **步骤 3:registry.ts** + +```ts +import type { BabylonBehavior } from "./types"; + +/** Per-adapter registry of Babylon behavior implementations, keyed by + * BehaviorDefinition.type. Mirrors ThreeBehaviorRegistry. */ +export class BabylonBehaviorRegistry { + private readonly behaviors = new Map(); + + register(b: BabylonBehavior): void { + const type = b.definition.type; + if (this.behaviors.has(type)) { + throw new Error(`BabylonBehaviorRegistry: duplicate type "${type}"`); + } + this.behaviors.set(type, b); + } + + get(type: string): BabylonBehavior | undefined { + return this.behaviors.get(type); + } + + list(): BabylonBehavior[] { + return [...this.behaviors.values()]; + } +} +``` + +运行 registry.test → PASS。 + +- [ ] **步骤 4:auto-rotate — 先写失败测试** + +创建 `src/runtime/babylon/behaviors/auto-rotate.test.ts`: + +```ts +import { NullEngine, Quaternion, Scene, TransformNode } from "@babylonjs/core"; +import { describe, expect, it } from "vitest"; + +import { AutoRotateBehavior } from "./auto-rotate"; + +describe("Babylon AutoRotateBehavior", () => { + it("has the auto-rotate definition", () => { + expect(new AutoRotateBehavior().definition.type).toBe("auto-rotate"); + }); + + it("tick rotates the node's quaternion around the axis", () => { + const engine = new NullEngine(); + const scene = new Scene(engine); + const node = new TransformNode("n", scene); + node.rotationQuaternion = Quaternion.Identity(); + const b = new AutoRotateBehavior(); + const handle = b.install(node, { axis: "y", speed: 90 }); + b.tick!(node, { axis: "y", speed: 90 }, handle, 1); // 90°/s · 1s = 90° + // 90° about Y → quaternion ≈ (0, sin45°, 0, cos45°) + expect(node.rotationQuaternion!.y).toBeCloseTo(Math.SQRT1_2, 5); + expect(node.rotationQuaternion!.w).toBeCloseTo(Math.SQRT1_2, 5); + scene.dispose(); + engine.dispose(); + }); +}); +``` + +运行 → FAIL(模块不存在)。 + +- [ ] **步骤 5:auto-rotate.ts** + +```ts +import { + Quaternion, + type Node as BabylonNode, + TransformNode, + Vector3, +} from "@babylonjs/core"; +import { z } from "zod"; + +import type { BehaviorDefinition } from "@/runtime/adapter"; + +import type { BabylonBehavior, BabylonBehaviorHandle } from "./types"; + +const ParamsSchema = z.object({ axis: z.enum(["x", "y", "z"]), speed: z.number() }); +type Params = z.infer; + +const DEG2RAD = Math.PI / 180; +const AXIS_VEC: Record = { + x: new Vector3(1, 0, 0), + y: new Vector3(0, 1, 0), + z: new Vector3(0, 0, 1), +}; + +export class AutoRotateBehavior implements BabylonBehavior { + readonly definition: BehaviorDefinition = { + type: "auto-rotate", + name: "Auto Rotate", + description: "Rotates the node around a local axis at a constant angular velocity.", + parameters_schema: ParamsSchema, + }; + + install(): BabylonBehaviorHandle { + return {}; + } + + tick( + node: BabylonNode, + params: Params, + _handle: BabylonBehaviorHandle, + dt: number, + ): void { + if (!(node instanceof TransformNode)) return; + const delta = Quaternion.RotationAxis( + AXIS_VEC[params.axis], + params.speed * DEG2RAD * dt, + ); + const cur = node.rotationQuaternion ?? Quaternion.Identity(); + node.rotationQuaternion = delta.multiply(cur); + } +} +``` + +运行 auto-rotate.test → PASS。 + +- [ ] **步骤 6:bob — 先写失败测试** + +创建 `src/runtime/babylon/behaviors/bob.test.ts`: + +```ts +import { NullEngine, Scene, TransformNode, Vector3 } from "@babylonjs/core"; +import { describe, expect, it } from "vitest"; + +import { BobBehavior } from "./bob"; + +describe("Babylon BobBehavior", () => { + it("has the bob definition", () => { + expect(new BobBehavior().definition.type).toBe("bob"); + }); + + it("tick sets position to base + amp*sin(2π f t)", () => { + const engine = new NullEngine(); + const scene = new Scene(engine); + const node = new TransformNode("n", scene); + node.position = new Vector3(0, 0, 0); + const params = { axis: "y" as const, amplitude: 2, frequency: 1 }; + const b = new BobBehavior(); + const handle = b.install(node, params); + b.tick!(node, params, handle, 0.25); // sin(2π·1·0.25)=sin(π/2)=1 → y=2 + expect(node.position.y).toBeCloseTo(2, 6); + b.tick!(node, params, handle, 0.25); // elapsed 0.5, sin(π)=0 → y=0 + expect(node.position.y).toBeCloseTo(0, 6); + scene.dispose(); + engine.dispose(); + }); +}); +``` + +运行 → FAIL。 + +- [ ] **步骤 7:bob.ts** + +```ts +import { type Node as BabylonNode, TransformNode } from "@babylonjs/core"; +import { z } from "zod"; + +import type { BehaviorDefinition } from "@/runtime/adapter"; + +import type { BabylonBehavior, BabylonBehaviorHandle } from "./types"; + +const ParamsSchema = z.object({ + axis: z.enum(["x", "y", "z"]), + amplitude: z.number(), + frequency: z.number(), +}); +type Params = z.infer; + +interface BobHandle extends BabylonBehaviorHandle { + base: number; + elapsed: number; +} + +const TWO_PI = Math.PI * 2; + +export class BobBehavior implements BabylonBehavior { + readonly definition: BehaviorDefinition = { + type: "bob", + name: "Bob", + description: "Floats the node up and down along a local axis (sine wave).", + parameters_schema: ParamsSchema, + }; + + install(node: BabylonNode, params: Params): BobHandle { + const base = node instanceof TransformNode ? node.position[params.axis] : 0; + return { base, elapsed: 0 }; + } + + tick(node: BabylonNode, params: Params, handle: BobHandle, dt: number): void { + if (!(node instanceof TransformNode)) return; + handle.elapsed += dt; + node.position[params.axis] = + handle.base + + params.amplitude * Math.sin(TWO_PI * params.frequency * handle.elapsed); + } +} +``` + +运行 bob.test → PASS。 + +- [ ] **步骤 8:index.ts** + +```ts +import { AutoRotateBehavior } from "./auto-rotate"; +import { BobBehavior } from "./bob"; +import { BabylonBehaviorRegistry } from "./registry"; + +export { BabylonBehaviorRegistry } from "./registry"; +export type { BabylonBehavior, BabylonBehaviorHandle } from "./types"; + +/** Build a registry pre-populated with the A2 behavior catalog (auto-rotate + + * bob). Per-adapter instance, mirroring createThreeBehaviorRegistry. */ +export function createBabylonBehaviorRegistry(): BabylonBehaviorRegistry { + const r = new BabylonBehaviorRegistry(); + r.register(new AutoRotateBehavior()); + r.register(new BobBehavior()); + return r; +} +``` + +- [ ] **步骤 9:全部测试 + typecheck** + +运行:`pnpm vitest run src/runtime/babylon/behaviors && pnpm typecheck` +预期:registry/auto-rotate/bob 测试全绿 + typecheck 0。 + +- [ ] **步骤 10:Commit** + +```bash +git add src/runtime/babylon/behaviors +git commit -m "feat(babylon): behavior framework — registry + auto-rotate + bob" +``` + +--- + +### 任务 2:契约三方法 + NoopAdapter stub + BabylonAdapter 实现 + +**文件:** 改 `src/runtime/adapter.ts`、`src/runtime/adapter.test.ts`、`src/runtime/babylon/adapter.ts`、`src/runtime/babylon/adapter.test.ts` + +> 契约加 3 方法会让 NoopAdapter + BabylonAdapter 立刻编译失败,故契约 + 两实现者同一任务,commit 保持绿。ThreeAdapter 已实现,无改动。 + +- [ ] **步骤 1:写失败的测试(BabylonAdapter behavior)** + +在 `src/runtime/babylon/adapter.test.ts` 末尾追加(该文件已有 `base`/`meshNode` helper;若无则按 A1 测试风格构造 group/mesh 节点): + +```ts +describe("BabylonAdapter behaviors", () => { + function meshAt(id: string, y: number): SceneNode { + return { + ...base, + id, + name: id, + type: "mesh", + parent_id: null, + transform: { position: [0, y, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] }, + data: { type: "mesh", geometry: { kind: "box" } }, + }; + } + + it("getSupportedBehaviors returns auto-rotate + bob", () => { + const types = new BabylonAdapter().getSupportedBehaviors().map((d) => d.type); + expect(types).toContain("auto-rotate"); + expect(types).toContain("bob"); + }); + + it("install + tick bob moves the node by the sine formula", () => { + const a = new BabylonAdapter(); + a.syncNode(meshAt("m", 0), "add"); + a.installBehaviors("m", [ + { + id: "b1", + behavior_type: "bob", + enabled: true, + parameters: { axis: "y", amplitude: 2, frequency: 1 }, + }, + ]); + a.tickBehaviors(0.25); + expect(a.describeNode("m")!.position[1]).toBeCloseTo(2, 6); + a.dispose(); + }); + + it("uninstall freezes the behavior", () => { + const a = new BabylonAdapter(); + a.syncNode(meshAt("m", 0), "add"); + a.installBehaviors("m", [ + { + id: "b1", + behavior_type: "bob", + enabled: true, + parameters: { axis: "y", amplitude: 2, frequency: 1 }, + }, + ]); + a.tickBehaviors(0.25); + a.uninstallBehaviors("m"); + const frozen = a.describeNode("m")!.position[1]; + a.tickBehaviors(0.25); + expect(a.describeNode("m")!.position[1]).toBe(frozen); + a.dispose(); + }); + + it("skips disabled bindings and isolates unknown types (no throw)", () => { + const a = new BabylonAdapter(); + a.syncNode(meshAt("m", 0), "add"); + expect(() => + a.installBehaviors("m", [ + { id: "x", behavior_type: "nope", enabled: true, parameters: {} }, + { + id: "d", + behavior_type: "bob", + enabled: false, + parameters: { axis: "y", amplitude: 2, frequency: 1 }, + }, + ]), + ).not.toThrow(); + a.tickBehaviors(0.25); + expect(a.describeNode("m")!.position[1]).toBe(0); // disabled bob didn't move it + a.dispose(); + }); +}); +``` + +(确认文件顶部已 `import type { SceneNode } from "@/core/scene/types";` 与 `base` helper;缺则补。) + +- [ ] **步骤 2:运行验证失败** + +运行:`pnpm vitest run src/runtime/babylon/adapter.test.ts` +预期:FAIL(`installBehaviors`/`getSupportedBehaviors` 现状抛 notImplemented / 不存在)。 + +- [ ] **步骤 3:契约加 3 方法(`src/runtime/adapter.ts`)** + +`IRuntimeAdapter` 的 Behaviors 段,`generateBehaviorCode` 那行之后加: + +```ts + /** Install behavior bindings on a synced node's runtime object. Disabled + * bindings are skipped; an invalid/unknown binding is isolated (logged via + * console.warn, not thrown) so one bad binding never blocks the rest. */ + installBehaviors(node_id: string, bindings: BehaviorBinding[]): void; + /** Advance every installed behavior by dt seconds. */ + tickBehaviors(dt: number): void; + /** Tear down all behaviors installed on a node (releasing per-binding state). */ + uninstallBehaviors(node_id: string): void; +``` + +- [ ] **步骤 4:NoopAdapter 补 stub(`src/runtime/adapter.test.ts`)** + +在 NoopAdapter 的 `dispose(): void {}` 附近加: + +```ts + installBehaviors(_node_id: string, _bindings: BehaviorBinding[]): void {} + tickBehaviors(_dt: number): void {} + uninstallBehaviors(_node_id: string): void {} +``` + +(`BehaviorBinding` 该文件应已 import;缺则在类型 import 补。) + +- [ ] **步骤 5:BabylonAdapter 实现(`src/runtime/babylon/adapter.ts`)** + +(a) import 区加: + +```ts +import { + createBabylonBehaviorRegistry, + type BabylonBehavior, + type BabylonBehaviorHandle, +} from "./behaviors"; +import type { BabylonBehaviorRegistry } from "./behaviors"; +``` + +`BehaviorBinding` 确认已在 `@/core/scene/types` import(A1 已 import 多个;缺则补)。`BehaviorDefinition` 已 import。 + +(b) 字段(`objects` map 旁)加: + +```ts + private readonly behaviorRegistry: BabylonBehaviorRegistry = createBabylonBehaviorRegistry(); + private readonly behaviorRuntime = new Map< + string, + Map + >(); +``` + +(c) `getSupportedBehaviors`(替换现 `notImplemented`): + +```ts + getSupportedBehaviors(): BehaviorDefinition[] { + return this.behaviorRegistry.list().map((b) => b.definition); + } +``` + +(d) 三方法(放在 `getSupportedBehaviors` 附近,`generateBehaviorCode` 仍 `notImplemented` 不动): + +```ts + installBehaviors(node_id: string, bindings: BehaviorBinding[]): void { + const node = this.objects.get(node_id); + if (!node) return; + const perNode = new Map< + string, + { behavior: BabylonBehavior; params: unknown; handle: BabylonBehaviorHandle } + >(); + for (const binding of bindings) { + if (!binding.enabled) continue; + const b = this.behaviorRegistry.get(binding.behavior_type); + if (!b) { + console.warn(`installBehaviors: unknown behavior_type "${binding.behavior_type}"`); + continue; + } + const parsed = b.definition.parameters_schema.safeParse(binding.parameters); + if (!parsed.success) { + console.warn( + `installBehaviors: invalid params on binding ${binding.id} (${binding.behavior_type})`, + ); + continue; + } + try { + const handle = b.install(node, parsed.data); + perNode.set(binding.id, { behavior: b, params: parsed.data, handle }); + } catch (e) { + console.error(`installBehaviors: install threw on ${binding.id}`, e); + } + } + this.behaviorRuntime.set(node_id, perNode); + } + + tickBehaviors(dt: number): void { + for (const [node_id, perNode] of this.behaviorRuntime) { + const node = this.objects.get(node_id); + if (!node) continue; + for (const entry of perNode.values()) { + try { + entry.behavior.tick?.(node, entry.params, entry.handle, dt); + } catch (e) { + console.error(`tickBehaviors: tick threw on node ${node_id}`, e); + } + } + } + } + + uninstallBehaviors(node_id: string): void { + const perNode = this.behaviorRuntime.get(node_id); + if (!perNode) return; + for (const entry of perNode.values()) { + try { + entry.handle.dispose?.(); + } catch (e) { + console.error("uninstallBehaviors: dispose threw", e); + } + } + this.behaviorRuntime.delete(node_id); + } +``` + +(e) `dispose()` 里清 behaviorRuntime——在现有 `this.objects.clear();` 旁加: + +```ts +this.behaviorRuntime.clear(); +``` + +- [ ] **步骤 6:运行验证通过** + +运行:`pnpm vitest run src/runtime/babylon/adapter.test.ts && pnpm typecheck` +预期:PASS(含新 4 用例)+ typecheck 0(契约 3 方法被 ThreeAdapter/BabylonAdapter/NoopAdapter 全满足)。 +(顺带 `pnpm vitest run src/runtime/three/adapter.test.ts` 确认 ThreeAdapter 不受影响。) + +- [ ] **步骤 7:Commit** + +```bash +git add src/runtime/adapter.ts src/runtime/adapter.test.ts src/runtime/babylon/adapter.ts src/runtime/babylon/adapter.test.ts +git commit -m "feat(adapter): behavior runtime on contract; BabylonAdapter install/tick/uninstall" +``` + +--- + +### 任务 3:conformance 扩展(behavior 跨引擎) + +**文件:** 改 `src/runtime/conformance/conformance-suite.ts`(追加 behavior `it`,跑双引擎) + +- [ ] **步骤 1:追加 behavior 断言** + +在 `conformance-suite.ts` 的 `describe(...)` 内、最后一个 `it`("reflects an updated transform" / "update missing throws")之后追加: + +```ts +it("getSupportedBehaviors includes auto-rotate + bob", () => { + const types = make() + .getSupportedBehaviors() + .map((d) => d.type); + expect(types).toContain("auto-rotate"); + expect(types).toContain("bob"); +}); + +it("bob behavior moves the node by base + amp*sin(2π f t) (exact across engines)", () => { + const a = make(); + a.syncNode( + node({ + id: "m", + name: "m", + type: "mesh", + data: { type: "mesh", geometry: { kind: "box" } }, + }), + "add", + ); + a.installBehaviors("m", [ + { + id: "b1", + behavior_type: "bob", + enabled: true, + parameters: { axis: "y", amplitude: 2, frequency: 1 }, + }, + ]); + a.tickBehaviors(0.25); // sin(π/2)=1 → y=2 + expect(a.describeNode("m")!.position[1]).toBeCloseTo(2, 6); + a.tickBehaviors(0.25); // sin(π)=0 → y=0 + expect(a.describeNode("m")!.position[1]).toBeCloseTo(0, 6); +}); + +it("auto-rotate behavior runs, accumulates, and freezes on uninstall", () => { + const a = make(); + a.syncNode( + node({ + id: "r", + name: "r", + type: "mesh", + data: { type: "mesh", geometry: { kind: "box" } }, + }), + "add", + ); + a.installBehaviors("r", [ + { + id: "b1", + behavior_type: "auto-rotate", + enabled: true, + parameters: { axis: "y", speed: 90 }, + }, + ]); + a.tickBehaviors(1); + const after1 = a.describeNode("r")!.rotation; + expect(after1).not.toEqual([0, 0, 0, 1]); // it rotated + a.tickBehaviors(1); + expect(a.describeNode("r")!.rotation).not.toEqual(after1); // it kept rotating + a.uninstallBehaviors("r"); + const frozen = a.describeNode("r")!.rotation; + a.tickBehaviors(1); + expect(a.describeNode("r")!.rotation).toEqual(frozen); // frozen after uninstall +}); + +it("skips disabled bindings and isolates unknown behavior types (no throw)", () => { + const a = make(); + a.syncNode( + node({ + id: "m", + name: "m", + type: "mesh", + data: { type: "mesh", geometry: { kind: "box" } }, + }), + "add", + ); + expect(() => + a.installBehaviors("m", [ + { id: "x", behavior_type: "nope", enabled: true, parameters: {} }, + { + id: "d", + behavior_type: "bob", + enabled: false, + parameters: { axis: "y", amplitude: 2, frequency: 1 }, + }, + ]), + ).not.toThrow(); + a.tickBehaviors(0.25); + expect(a.describeNode("m")!.position[1]).toBe(0); // disabled bob didn't move it +}); +``` + +(这 4 个 `it` 对 ThreeAdapter + BabylonAdapter 各跑一遍——`make()` 已追踪并 `afterEach` dispose;unknown type 会打 `console.warn`,是隔离生效的预期输出。) + +- [ ] **步骤 2:运行验证** + +运行:`pnpm vitest run src/runtime/conformance/conformance.test.ts` +预期:**两个 label(ThreeAdapter / BabylonAdapter)下全部用例 PASS**(bob 精确对等、auto-rotate ran/累积/卸载、enabled/unknown 隔离)。 +(若 Babylon 某条失败:先判断是真实现 bug(改 BabylonAdapter/behavior)还是断言写法;bob 两引擎同公式应一致;auto-rotate 只验 ran/累积/卸载不应触发跨引擎逐位比较。卡住超两次 BLOCKED 上报。) + +整体确认:`pnpm vitest run src/runtime && pnpm typecheck`。 + +- [ ] **步骤 3:Commit** + +```bash +git add src/runtime/conformance/conformance-suite.ts +git commit -m "test(conformance): behavior runtime parity — bob exact, auto-rotate ran (both engines)" +``` + +--- + +### 任务 4:全量验证 + 收尾 + +- [ ] **步骤 1:全绿** + +运行:`pnpm lint && pnpm typecheck && pnpm test` +预期:0 error / 全绿。**无视觉验证**(A2 纯 headless)。 + +- [ ] **步骤 2:收尾** + +用 superpowers:finishing-a-development-branch:commit plan + roadmap(v1.0 sub-stage A2 勾选 + PR 链接)+ 开 PR。PR 描述写明:A2 验证了什么(behavior 运行时契约 + bob 精确跨引擎对等 + auto-rotate ran/累积/卸载)、无视觉验证理由、A3/B 后续。 + +--- + +## 自检 + +**1. 规格覆盖度(spec §1 成功标准逐条):** + +- 契约含三方法、三实现者满足 → 任务 2(契约 + NoopAdapter stub + BabylonAdapter;ThreeAdapter 已有)✓ +- conformance bob 精确对等 → 任务 3 ✓ +- conformance auto-rotate ran/累积/卸载 → 任务 3 ✓ +- enabled:false 不生效 + unknown 隔离不抛 → 任务 2(adapter 单测)+ 任务 3(conformance)✓ +- getSupportedBehaviors 含俩 → 任务 1(registry)+ 任务 2(adapter)+ 任务 3(conformance)✓ +- lint/type/test 全绿、无视觉 → 任务 4 ✓ + +**2. 占位符扫描:** 各任务完整可粘贴代码 + 命令 + 预期;无 TODO/待定。✓ + +**3. 类型一致性:** + +- `BabylonBehavior`/`BabylonBehaviorHandle`(任务 1 types.ts)→ 任务 1 registry/auto-rotate/bob、任务 2 adapter 字段一致引用 ✓ +- `createBabylonBehaviorRegistry()`(任务 1 index)→ 任务 2 BabylonAdapter 字段调用 ✓ +- 契约 `installBehaviors(node_id, bindings)` / `tickBehaviors(dt)` / `uninstallBehaviors(node_id)`(任务 2 adapter.ts)→ ThreeAdapter 现有签名一致、BabylonAdapter 同签名、conformance 同调用 ✓ +- behavior 参数 schema(auto-rotate: axis+speed;bob: axis+amplitude+frequency)两引擎一致 → bob 精确对等成立 ✓ +- `BehaviorBinding` 形状 `{id, behavior_type, enabled, parameters}`(任务 2/3 fixture)与 schema 一致 ✓ + +**4. 边界:** 未 sync 的 node install → no-op;未知 type/非法 params → console.warn 隔离;install/tick 抛 → console.error 隔离;非 TransformNode → behavior 跳过;dispose 清 behaviorRuntime。均覆盖。 + +**5. 每 commit 绿**:任务 1 leaf standalone;任务 2 契约+两实现者同 commit(NoopAdapter+BabylonAdapter 同步,ThreeAdapter 已满足);任务 3 conformance;任务 4 验证。无破绿中间态。 + +发现即修,无遗留。 + +--- + +## 执行交接 + +计划已保存到 `docs/superpowers/plans/2026-06-10-v1.0a2-babylon-behaviors-plan.md`。两种执行方式: + +1. **子代理驱动(推荐)** —— 任务 1-2 机械 TDD;任务 3 conformance 是关键交付,做完审查;任务 4 收尾。 +2. **内联执行** —— 当前会话用 executing-plans 批量 + 检查点。 + +选哪种? From c3e94b58d6e7709c65773d57afa856e610236103 Mon Sep 17 00:00:00 2001 From: longyi-xw <2691049525@qq.com> Date: Wed, 10 Jun 2026 12:29:05 +0800 Subject: [PATCH 7/7] =?UTF-8?q?docs(roadmap):=20v1.0=20A2=20behaviors=20?= =?UTF-8?q?=E8=B7=A8=E5=BC=95=E6=93=8E=E8=BF=90=E8=A1=8C=E6=97=B6=20?= =?UTF-8?q?=E5=AE=8C=E6=88=90=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- docs/roadmap.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 047e753..075f1db 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -120,8 +120,8 @@ - **Goals**: 多运行时适配器(架构 §7 v1.0)。把 `IRuntimeAdapter` 确立为引擎中立接缝,用第二个引擎(Babylon.js)+ conformance 套件验证抽象成立;最终支持实时切换渲染引擎、新引擎最小改动接入(项目本质:多框架低代码 3D 平台)。 - **Sub-stages**: - [x] **A1 · 契约 + Babylon headless + conformance**([#37](https://github.com/longyi-xw/lowcode-3d/pull/37)):契约加引擎中立 `describeNode(): RuntimeNodeInfo`(读引擎对象)+ `dispose()`;`BabylonAdapter`(`@babylonjs/core` `NullEngine`,headless)实现 `syncNode` 核心 kind(group/mesh/light/camera);conformance 套件让 ThreeAdapter + BabylonAdapter 跑同一批断言全绿 = 契约对第二引擎成立。纯 headless,不接实时视口。 - - [ ] **A2 · behaviors 跨引擎**:install/tick + behavior codegen 在 Babylon 对等。 - - [ ] **A3 · glTF + 导出**:prefab_instance/glTF 加载 + `babylon` 导出 target。 + - [x] **A2 · behaviors 跨引擎运行时**([#38](https://github.com/longyi-xw/lowcode-3d/pull/38)):`installBehaviors`/`tickBehaviors`/`uninstallBehaviors` 纳入 `IRuntimeAdapter`;Babylon behavior 框架(registry + auto-rotate + bob,操作 Babylon 节点,共享引擎中立 `BehaviorDefinition`);conformance 验运行时对等——**bob 跨引擎精确对等**(位置突变同公式)、auto-rotate ran/累积/卸载、enabled/unknown 隔离。纯 headless。hover-highlight(事件型)+ behavior codegen 留后续。 + - [ ] **A3 · glTF + 导出**:prefab_instance/glTF 加载 + `babylon` 导出 target(含 behavior codegen 跨引擎)。 - [ ] **B · 实时视口切引擎**:抽 `IRenderHost`(渲染循环/相机控制/gizmo/拾取/outline),ThreeViewport 改为面向它,Babylon 渲染宿主落地,用户可把实时编辑器切到 Babylon。spec §8 已备边界清单。 - **Depends on**: v0.5 行为系统全部完成(v0.5 Stage C) @@ -136,7 +136,7 @@ - **材质贴图 pipeline**(从 v0.2 材质编辑拆出):normalMap / map / roughnessMap / metalnessMap / aoMap 等。需 `MaterialOverrideSchema` 加贴图字段(引用 `kind:"texture"` 的 `AssetReference`)+ texture 资源上传入库 + runtime `TextureLoader`(`mesh.ts applyOverrides` 加贴图通道)+ codegen emit `TextureLoader().load("./assets/…")` + UI 贴图选择器。独立子系统,单独 spec→plan→实现。 - **Socket 系统后续阶段**(从 v0.4 sub-stage C 地基拆出):本期只做位置对齐 + tag 兼容 + 面板手填。延后(各自独立 spec→plan→实现):**朝向咬合**(socket 带 normal/up,转动节点让两 socket 面对面「插上」)、**模型特性驱动的对齐规则引擎**(超越 tag 精确匹配)、**glb 内嵌 socket 自动识别**(importer 读 glTF 命名空节点)、**视口点选放置 socket**(raycast 模型表面落点)、**socket 类型库 / male-female 配对 / 批量拼装**、**socket 标记 LOD / 合批**(本期每次拖拽全量重建标记,大场景需按节点索引只刷被拖节点——视觉验证审查记录的次要优化项)。 -- **Babylon 适配器跨引擎差异(A1 记录,待 in-scope 时对齐)**:A1 conformance 只在 group/mesh 断言完整 transform,已知未对齐项延后到各自变 in-scope 时处理——(a) **camera 朝向**:Babylon `UniversalCamera` 用 Euler `.rotation`(无 `rotationQuaternion`),`applyBabylonTransform` 当前跳过其旋转;(b) **ambient light**:映射为 `HemisphericLight`,无 `position`,落点被丢;(c) **prefab_instance kind**:ThreeAdapter 占位报 `mesh`、BabylonAdapter 报 `unknown`(A3 加 glTF 时统一)。camera/light 朝向并入 A2/B,prefab 并入 A3。 +- **Babylon 适配器跨引擎差异(A1/A2 记录,待 in-scope 时对齐)**:conformance 已断言的部分两引擎对等;已知未对齐/未处理项——(a) **camera 朝向**:Babylon `UniversalCamera` 用 Euler `.rotation`(无 `rotationQuaternion`),`applyBabylonTransform` 当前跳过其旋转(并入 B);(b) **ambient light**:映射为 `HemisphericLight`,无 `position`,落点被丢(并入 B);(c) **prefab_instance kind**:ThreeAdapter 占位报 `mesh`、BabylonAdapter 报 `unknown`(A3 加 glTF 时统一);(d) **BabylonAdapter.dispose() 的 behavior handle 释放**:当前 `behaviorRuntime.clear()` 不遍历调 `handle.dispose?.()`(A2 的 auto-rotate/bob handle 无 dispose,故当前无副作用);将来加有状态 Babylon behavior(绑监听器等)时,dispose 应改为遍历 `uninstallBehaviors` 释放 handle。 - **多源资源上传**(从 v0.2 资源库拆出):`AssetSourceSchema` 已含 `builtin/user_upload/online/ai_generated` 且 catalog 来源无关;目前只实装 `builtin`(几何/灯光预设)+ `user_upload`(.glb)。延后:`online`(在线模型/材质库浏览+下载入库)、`ai_generated`(AI 生成模型/贴图)。 - **多材质槽**(从 v0.2 材质编辑拆出):本期材质编辑只支持 slot 0;prefab/glTF 多材质对象的 slot 1+ 延后。 - **agentic 多轮 Skill 执行**(从 v0.3 sub-stage B 拆出):本期 Skill 是**单轮结构化输出**(NL→LLM 一次返回 operations→Command)。延后:架构 §4.3 的 `call_tool` / `allowed_tools` 多轮 agent 循环(LLM 多轮调工具、读场景、迭代纠错)+ `SkillContext.memory`(`MemoryStore` 项目暂无实现)。用户明确「多轮后续肯定要完善」。