Restructure core-v2 into layered modules

Rewrite the core-v2 internals without changing the public API or runtime
behavior. The four large files (types, scene, motion, gesture) become
layered modules with one responsibility each:

- shared/: geometry and environment helpers
- view/: originView recipes
- choreography/: effect types, presets, and composeEffects compositing
- actions/: forward/back/replace/above/under via one overload resolver
- scene/: SceneGraph storage, transitions (history rules), compositor
  (styles and interactivity), spring (pure stepper + rAF driver), and
  createOriginScene, which only wires them and owns async timing
- composables/: useOrigin and the node-scope injection key
- gesture/: builder, definition->policy normalization, start admission,
  DirectionalTracker, and a GestureRecognizer session state machine
  replacing fifteen loose closure variables
- components/: renderers and gesture hosts, plus their prop types

Verification (packages/core-v2/verify): differential fuzzing drives
main and this branch with identical random operation sequences and
pointer streams and requires identical scene state, styles, contexts,
callbacks, and promise results at every step. It caught every planted
mutation that applied. Benchmarks are at parity.

Also fix a pre-existing test that called the defineProps macro inside a
runtime defineComponent, and correct API.md's description of the flick
commit threshold to match the runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Faarh2Ts6StJPcz2mRiHiR
This commit is contained in:
Claude
2026-10-16 01:15:12 +00:00
parent d8303107fb
commit 329271691c
48 changed files with 4134 additions and 2681 deletions
+110
View File
@@ -0,0 +1,110 @@
import { defineComponent, h, type Component } from "vue";
import type { OriginScene } from "../src";
export function rng(seed: number) {
let a = seed >>> 0;
const next = () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
return {
next,
int: (n: number) => Math.floor(next() * n),
pick: <T>(items: readonly T[]) => items[Math.floor(next() * items.length)]!,
chance: (p: number) => next() < p,
};
}
export function component(name: string): Component {
return defineComponent({ name, render: () => h("div", name) });
}
/** Deterministic rAF + clock shared by both implementations. */
export function installFrames() {
let now = 1000;
let queue: FrameRequestCallback[] = [];
window.requestAnimationFrame = ((cb: FrameRequestCallback) => {
queue.push(cb);
return queue.length;
}) as typeof window.requestAnimationFrame;
globalThis.requestAnimationFrame = window.requestAnimationFrame;
const perf = performance;
perf.now = () => now;
return {
advance(ms = 16) {
now += ms;
const run = queue;
queue = [];
for (const cb of run) cb(now);
},
pending: () => queue.length,
};
}
export async function flush(rounds = 6) {
for (let i = 0; i < rounds; i += 1)
await new Promise<void>((resolve) => setTimeout(resolve, 0));
}
/** Implementation-agnostic, comparable snapshot of a scene. */
export function snapshot(scene: OriginScene) {
const nodes = scene.nodes.value;
return {
nodes: nodes.map((node) => ({
key: node.key,
sequence: node.sequence,
view: node.view.key,
previousNodeKey: node.previousNodeKey,
state: node.state,
incomingOperationId: node.incomingOperationId,
history: node.history.map((view) => view.key),
})),
operations: scene.operations.value.map((operation) => ({
id: operation.id,
originKey: operation.originKey,
targetKey: operation.targetKey,
entryTargetKey: operation.entryTargetKey,
choreography: operation.choreography.name,
placement: operation.placement,
history: operation.history,
progress: operation.progress,
velocity: operation.velocity,
phase: operation.phase,
intent: operation.intent,
})),
roots: [...scene.roots.value],
styles: Object.fromEntries(
nodes.map((node) => [node.key, scene.styleForNode(node.key)]),
),
interactive: Object.fromEntries(
nodes.map((node) => [node.key, scene.isNodeInteractive(node.key)]),
),
contexts: Object.fromEntries(
nodes.map((node) => {
const context = scene.contextFor(node.key);
return [
node.key,
{
nodeKey: context.nodeKey,
view: context.view.key,
canGoBack: context.canGoBack,
previous: context.previous?.key,
history: context.history.map((view) => view.key),
},
];
}),
),
missingStyle: scene.styleForNode("missing::0"),
missingInteractive: scene.isNodeInteractive("missing::0"),
};
}
export async function settle<T>(promise: Promise<T>) {
return promise.then(
(value) => ({ ok: true as const, value }),
(error: Error) => ({ ok: false as const, error: error.message }),
);
}