Consolidate core-v2 modules and move scene logic onto classes
- Merge the small modules into 12 files: shared, choreography,
navigation, scene/{types,graph,scene}, gesture/{definition,recognizer}.
- SceneGraph now owns the begin/update/commit/cancel history rules as
methods, with the step helpers as private methods.
- createOriginScene returns a markRaw'd Scene class; operation handles
are an OperationHandle class instead of inline object literals.
- GestureRecognizer gains static create()/policyFor() factories, absorbing
the separate policy module.
- Pure helpers used by classes (matchesStart, pointOf, stepSpring,
composeNodeStyle, nodeEffectLayers, shouldCommit, ...) are exported
consts so they can be tested directly.
- Add haptics tests showing where navigator.vibrate can be triggered.
Public exports, types, and behavior are unchanged; the differential
suites against main still pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Faarh2Ts6StJPcz2mRiHiR
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
import type { CSSProperties } from "vue";
|
||||
import type { OriginChoreography } from "../choreography";
|
||||
import type { MaybeOriginAction, OriginNavigationIntent } from "../navigation";
|
||||
import type { OriginContext } from "../scene/types";
|
||||
import type { OriginRect } from "../shared";
|
||||
|
||||
/*
|
||||
* What a gesture is: the public gesture types, component props, and the
|
||||
* immutable builder that produces executable definitions. Running them is
|
||||
* the recognizer's job.
|
||||
*/
|
||||
|
||||
/** Direction in which pointer movement advances operation progress. */
|
||||
export type OriginGestureDirection = "left" | "right" | "up" | "down";
|
||||
|
||||
/** Physical side of the gesture host used to admit pointer-down. */
|
||||
export type OriginGestureEdge = "left" | "right" | "top" | "bottom";
|
||||
|
||||
/**
|
||||
* CSS length accepted by an edge-based gesture start rule.
|
||||
*
|
||||
* Numbers are interpreted as CSS pixels. Strings may use normal CSS lengths,
|
||||
* percentages, `calc()`, or `clamp()`, and are resolved against the gesture
|
||||
* host when pointer-down occurs.
|
||||
*/
|
||||
export type OriginGestureDistance = number | string;
|
||||
|
||||
/** A pointer position measured in viewport and gesture-host coordinates. */
|
||||
export interface OriginGesturePoint {
|
||||
/** Viewport-relative horizontal position. */
|
||||
readonly clientX: number;
|
||||
/** Viewport-relative vertical position. */
|
||||
readonly clientY: number;
|
||||
/** Horizontal position relative to the gesture host's left edge. */
|
||||
readonly localX: number;
|
||||
/** Vertical position relative to the gesture host's top edge. */
|
||||
readonly localY: number;
|
||||
}
|
||||
|
||||
/** Context supplied to a custom `.from.when()` start predicate. */
|
||||
export interface OriginGestureStartContext {
|
||||
/** Native pointer-down event being considered. */
|
||||
readonly event: PointerEvent;
|
||||
/** Component instance from which the gesture would originate. */
|
||||
readonly origin: OriginContext;
|
||||
/** Element carrying the gesture's pointer handlers. */
|
||||
readonly host: HTMLElement;
|
||||
/** Gesture-host bounds captured at pointer-down. */
|
||||
readonly bounds: OriginRect;
|
||||
/** Pointer position at pointer-down. */
|
||||
readonly point: OriginGesturePoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous predicate deciding whether a pointer-down may become a gesture.
|
||||
*
|
||||
* Directional recognition still occurs later, after movement passes the
|
||||
* configured intent threshold.
|
||||
*/
|
||||
export type OriginGestureStartPredicate = (
|
||||
context: OriginGestureStartContext,
|
||||
) => boolean;
|
||||
|
||||
/** Start policy stored in a completed gesture definition. */
|
||||
export type OriginGestureStart =
|
||||
| {
|
||||
/** Recognize pointer-down anywhere on the host. */
|
||||
readonly kind: "anywhere";
|
||||
}
|
||||
| {
|
||||
/** Recognize pointer-down within a CSS distance of one host side. */
|
||||
readonly kind: "edge";
|
||||
readonly edge: OriginGestureEdge;
|
||||
readonly distance: OriginGestureDistance;
|
||||
}
|
||||
| {
|
||||
/** Recognize pointer-down when application policy returns `true`. */
|
||||
readonly kind: "when";
|
||||
readonly predicate: OriginGestureStartPredicate;
|
||||
};
|
||||
|
||||
/** Direction-recognition tuning accepted by `.to.left()` and its siblings. */
|
||||
export interface OriginGestureDirectionOptions {
|
||||
/**
|
||||
* Minimum directed movement in CSS pixels before the gesture captures.
|
||||
*
|
||||
* @defaultValue `8`
|
||||
*/
|
||||
readonly threshold?: number;
|
||||
/**
|
||||
* Ratio by which directed movement must exceed cross-axis movement.
|
||||
*
|
||||
* @defaultValue `1.15`
|
||||
*/
|
||||
readonly axisDominance?: number;
|
||||
}
|
||||
|
||||
/** Values available when a custom `.complete()` policy runs on pointer-up. */
|
||||
export interface OriginGestureCompletionContext {
|
||||
/** Component instance that originated the gesture. */
|
||||
readonly origin: OriginContext;
|
||||
/** Recognized movement direction. */
|
||||
readonly direction: OriginGestureDirection;
|
||||
/** Normalized directed distance, clamped to `0..1`. */
|
||||
readonly progress: number;
|
||||
/** Latest normalized progress units per second. */
|
||||
readonly velocity: number;
|
||||
/** Directed movement from pointer-down in CSS pixels. */
|
||||
readonly distance: number;
|
||||
/** Absolute cross-axis movement from pointer-down in CSS pixels. */
|
||||
readonly crossDistance: number;
|
||||
/** Elapsed time since pointer-down in milliseconds. */
|
||||
readonly duration: number;
|
||||
/** Native pointer-up event that ended the interaction. */
|
||||
readonly event: PointerEvent;
|
||||
/** Element carrying the gesture's pointer handlers. */
|
||||
readonly host: HTMLElement;
|
||||
/** Gesture-host bounds captured at pointer-down. */
|
||||
readonly bounds: OriginRect;
|
||||
/** Pointer position captured at pointer-down. */
|
||||
readonly start: OriginGesturePoint;
|
||||
/** Pointer position at release. */
|
||||
readonly current: OriginGesturePoint;
|
||||
}
|
||||
|
||||
/** Synchronous commit/cancel policy installed by `.complete()`. */
|
||||
export type OriginGestureCompletionPredicate = (
|
||||
context: OriginGestureCompletionContext,
|
||||
) => boolean;
|
||||
|
||||
/**
|
||||
* Values accepted from a gesture builder's `.navigate()` factory.
|
||||
*
|
||||
* A promise supports lazy view selection. Stale resolutions are discarded if
|
||||
* the pointer has already ended or been cancelled.
|
||||
*/
|
||||
export type MaybeOriginNavigationIntent =
|
||||
| OriginNavigationIntent
|
||||
| null
|
||||
| undefined
|
||||
| Promise<OriginNavigationIntent | null | undefined>;
|
||||
|
||||
/** Factory that resolves navigation after directional recognition succeeds. */
|
||||
export type OriginGestureNavigationFactory = (
|
||||
context: OriginContext,
|
||||
) => MaybeOriginNavigationIntent;
|
||||
|
||||
/**
|
||||
* Immutable, executable result of a complete gesture builder chain.
|
||||
*
|
||||
* Pass this object to {@link useOriginGesture}. An omitted `.from` step is
|
||||
* represented as an `anywhere` start rule.
|
||||
*/
|
||||
export interface OriginGestureDefinition {
|
||||
/** Discriminator used by the compatibility recognizer overload. */
|
||||
readonly kind: "origin-gesture-definition";
|
||||
/** Pointer-down eligibility policy. */
|
||||
readonly start: OriginGestureStart;
|
||||
/** Direction in which movement advances operation progress. */
|
||||
readonly direction: OriginGestureDirection;
|
||||
/** Directional intent recognition tuning. */
|
||||
readonly recognition: Readonly<OriginGestureDirectionOptions>;
|
||||
/**
|
||||
* Optional release decision. When omitted, choreography thresholds decide.
|
||||
*/
|
||||
readonly completion?: OriginGestureCompletionPredicate;
|
||||
/** Node-local destination/history resolver. */
|
||||
readonly navigation: OriginGestureNavigationFactory;
|
||||
/** Visual routine paired with the navigation intent. */
|
||||
readonly choreography: OriginChoreography;
|
||||
}
|
||||
|
||||
/** Direction-selection stage shared by `gesture.to` and `.from.*().to`. */
|
||||
export interface OriginGestureToBuilder {
|
||||
/** Recognize leftward pointer movement. */
|
||||
left(options?: OriginGestureDirectionOptions): OriginGestureDirectedBuilder;
|
||||
/** Recognize rightward pointer movement. */
|
||||
right(options?: OriginGestureDirectionOptions): OriginGestureDirectedBuilder;
|
||||
/** Recognize upward pointer movement. */
|
||||
up(options?: OriginGestureDirectionOptions): OriginGestureDirectedBuilder;
|
||||
/** Recognize downward pointer movement. */
|
||||
down(options?: OriginGestureDirectionOptions): OriginGestureDirectedBuilder;
|
||||
}
|
||||
|
||||
/** Stage produced after a `.from` policy has been selected. */
|
||||
export interface OriginGestureFromSelection {
|
||||
/** Select the direction that advances this gesture. */
|
||||
readonly to: OriginGestureToBuilder;
|
||||
}
|
||||
|
||||
/** Optional pointer-down policy exposed at the start of a gesture chain. */
|
||||
export interface OriginGestureFromBuilder {
|
||||
/** Admit pointer-down within `distance` of the host's left edge. */
|
||||
left(distance: OriginGestureDistance): OriginGestureFromSelection;
|
||||
/** Admit pointer-down within `distance` of the host's right edge. */
|
||||
right(distance: OriginGestureDistance): OriginGestureFromSelection;
|
||||
/** Admit pointer-down within `distance` of the host's top edge. */
|
||||
top(distance: OriginGestureDistance): OriginGestureFromSelection;
|
||||
/** Admit pointer-down within `distance` of the host's bottom edge. */
|
||||
bottom(distance: OriginGestureDistance): OriginGestureFromSelection;
|
||||
/** Admit pointer-down anywhere on the gesture host. */
|
||||
anywhere(): OriginGestureFromSelection;
|
||||
/** Admit pointer-down when a synchronous application predicate succeeds. */
|
||||
when(predicate: OriginGestureStartPredicate): OriginGestureFromSelection;
|
||||
}
|
||||
|
||||
/** Builder stage after direction is known and completion remains optional. */
|
||||
export interface OriginGestureDirectedBuilder {
|
||||
/** Override the choreography's default release decision. */
|
||||
complete(
|
||||
predicate: OriginGestureCompletionPredicate,
|
||||
): OriginGestureCompletedBuilder;
|
||||
/** Select the target/history mutation while retaining default completion. */
|
||||
navigate(
|
||||
factory: OriginGestureNavigationFactory,
|
||||
): OriginGestureNavigationBuilder;
|
||||
}
|
||||
|
||||
/** Builder stage after a custom completion policy has been selected. */
|
||||
export interface OriginGestureCompletedBuilder {
|
||||
/** Select the target/history mutation performed on commit. */
|
||||
navigate(
|
||||
factory: OriginGestureNavigationFactory,
|
||||
): OriginGestureNavigationBuilder;
|
||||
}
|
||||
|
||||
/** Final builder stage waiting for visual choreography. */
|
||||
export interface OriginGestureNavigationBuilder {
|
||||
/** Attach visual choreography and produce an executable definition. */
|
||||
animate(choreography: OriginChoreography): OriginGestureDefinition;
|
||||
}
|
||||
|
||||
/** Root of the immutable fluent gesture-definition API. */
|
||||
export interface OriginGestureBuilder {
|
||||
/** Optionally constrain where pointer-down may begin. */
|
||||
readonly from: OriginGestureFromBuilder;
|
||||
/**
|
||||
* Select movement direction with an implicit `from.anywhere()` start.
|
||||
*/
|
||||
readonly to: OriginGestureToBuilder;
|
||||
}
|
||||
|
||||
/** Configuration consumed by {@link useOriginGesture}. */
|
||||
export interface OriginGestureOptions {
|
||||
/**
|
||||
* Direction in which the pointer moves to advance the operation.
|
||||
*
|
||||
* The starting edge is the opposite side: `right` begins at the left edge,
|
||||
* `left` at the right edge, `down` at the top, and `up` at the bottom.
|
||||
*/
|
||||
direction: OriginGestureDirection;
|
||||
/**
|
||||
* Restrict pointer-down to this many CSS pixels from the gesture host's
|
||||
* starting edge. Omit it to recognize across the entire host.
|
||||
*
|
||||
* This is relative to the bound element, not necessarily the browser
|
||||
* viewport. Use a positive number such as `24` or `36`.
|
||||
*/
|
||||
edge?: number;
|
||||
/**
|
||||
* Minimum directed movement in CSS pixels before the gesture captures.
|
||||
*
|
||||
* Movement must also dominate the cross-axis by a factor of `1.15`.
|
||||
*
|
||||
* @defaultValue `8`
|
||||
*/
|
||||
threshold?: number;
|
||||
/**
|
||||
* Resolve the scene action after directional recognition succeeds.
|
||||
*
|
||||
* Returning no action abandons recognition without modifying the scene.
|
||||
*/
|
||||
action(context: OriginContext): MaybeOriginAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* DOM bindings returned by {@link useOriginGesture}.
|
||||
*
|
||||
* Spread or attach all four handlers to the same `HTMLElement`. Apply
|
||||
* {@link style} as well so native scrolling is preserved on the cross-axis.
|
||||
*/
|
||||
export interface OriginGestureBinding {
|
||||
/** Required size and `touch-action` styles for the gesture host. */
|
||||
readonly style: Readonly<CSSProperties>;
|
||||
/** Pointer-down handler that records a potentially eligible gesture. */
|
||||
readonly onPointerdown: (event: PointerEvent) => void;
|
||||
/** Pointer-move handler that recognizes and updates the operation. */
|
||||
readonly onPointermove: (event: PointerEvent) => void;
|
||||
/** Pointer-up handler that commits or cancels using progress and velocity. */
|
||||
readonly onPointerup: (event: PointerEvent) => void;
|
||||
/** Pointer-cancel handler that abandons any captured operation. */
|
||||
readonly onPointercancel: () => void;
|
||||
}
|
||||
|
||||
// Component props ----------------------------------------------------------
|
||||
|
||||
/** Props shared by both `OriginGesture` declaration styles. */
|
||||
export interface OriginGestureBaseProps {
|
||||
/**
|
||||
* Native HTML tag rendered as the gesture host.
|
||||
*
|
||||
* @defaultValue `"div"`
|
||||
*/
|
||||
as?: string;
|
||||
}
|
||||
|
||||
/** Builder-definition props accepted by the `OriginGesture` component. */
|
||||
export interface OriginGestureDefinitionProps extends OriginGestureBaseProps {
|
||||
/** Immutable definition produced by the {@link gesture} builder. */
|
||||
gesture: OriginGestureDefinition;
|
||||
/** Builder definitions already contain direction. */
|
||||
direction?: never;
|
||||
/** Builder definitions already contain their start policy. */
|
||||
edge?: never;
|
||||
/** Builder definitions already contain recognition tuning. */
|
||||
threshold?: never;
|
||||
/** Builder definitions already contain their navigation factory. */
|
||||
action?: never;
|
||||
}
|
||||
|
||||
/** Legacy option props accepted by the `OriginGesture` component. */
|
||||
export interface OriginGestureLegacyProps extends OriginGestureBaseProps {
|
||||
/** Legacy component declarations do not provide a builder definition. */
|
||||
gesture?: never;
|
||||
/** Direction in which pointer movement advances operation progress. */
|
||||
direction: OriginGestureDirection;
|
||||
/**
|
||||
* Eligible pointer-down width in CSS pixels from the host's starting edge.
|
||||
* Omit it to allow the full component surface.
|
||||
*/
|
||||
edge?: number;
|
||||
/**
|
||||
* Directed movement required before capture.
|
||||
*
|
||||
* @defaultValue `8`
|
||||
*/
|
||||
threshold?: number;
|
||||
/** Node-local action factory invoked only after recognition succeeds. */
|
||||
action(context: OriginContext): MaybeOriginAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public props accepted by the `OriginGesture` convenience component.
|
||||
*
|
||||
* Prefer the builder-definition form. The legacy direction/action form remains
|
||||
* available for compatibility.
|
||||
*/
|
||||
export type OriginGestureProps =
|
||||
OriginGestureDefinitionProps | OriginGestureLegacyProps;
|
||||
|
||||
/** Public props accepted by the multi-definition gesture surface component. */
|
||||
export interface OriginGestureSurfaceProps {
|
||||
/**
|
||||
* Native HTML tag rendered as the shared gesture host.
|
||||
*
|
||||
* @defaultValue `"div"`
|
||||
*/
|
||||
as?: string;
|
||||
/**
|
||||
* Complete immutable definitions installed on the shared host.
|
||||
*
|
||||
* The surface adds no start, direction, completion, navigation, or animation
|
||||
* policy. Definitions should be created by the owning page component.
|
||||
*/
|
||||
gestures: readonly OriginGestureDefinition[];
|
||||
}
|
||||
|
||||
// Builder ------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* Immutable fluent builder. Each stage is a frozen object closing over the
|
||||
* choices made so far; the final `.animate()` produces a frozen definition.
|
||||
*
|
||||
* gesture[.from.<start>].to.<direction>()[.complete()].navigate().animate()
|
||||
*/
|
||||
|
||||
function navigationStage(
|
||||
start: OriginGestureStart,
|
||||
direction: OriginGestureDirection,
|
||||
recognition: Readonly<OriginGestureDirectionOptions>,
|
||||
completion: OriginGestureCompletionPredicate | undefined,
|
||||
) {
|
||||
return (
|
||||
navigation: OriginGestureNavigationFactory,
|
||||
): OriginGestureNavigationBuilder =>
|
||||
Object.freeze({
|
||||
animate: (choreography: OriginChoreography): OriginGestureDefinition =>
|
||||
Object.freeze({
|
||||
kind: "origin-gesture-definition",
|
||||
start,
|
||||
direction,
|
||||
recognition,
|
||||
completion,
|
||||
navigation,
|
||||
choreography,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function directedStage(
|
||||
start: OriginGestureStart,
|
||||
direction: OriginGestureDirection,
|
||||
options: OriginGestureDirectionOptions = {},
|
||||
): OriginGestureDirectedBuilder {
|
||||
const recognition = Object.freeze({ ...options });
|
||||
return Object.freeze({
|
||||
complete: (completion: OriginGestureCompletionPredicate) =>
|
||||
Object.freeze({
|
||||
navigate: navigationStage(start, direction, recognition, completion),
|
||||
}),
|
||||
navigate: navigationStage(start, direction, recognition, undefined),
|
||||
});
|
||||
}
|
||||
|
||||
function toStage(start: OriginGestureStart): OriginGestureToBuilder {
|
||||
const direct =
|
||||
(direction: OriginGestureDirection) =>
|
||||
(options?: OriginGestureDirectionOptions) =>
|
||||
directedStage(start, direction, options);
|
||||
return Object.freeze({
|
||||
left: direct("left"),
|
||||
right: direct("right"),
|
||||
up: direct("up"),
|
||||
down: direct("down"),
|
||||
});
|
||||
}
|
||||
|
||||
function fromStage(start: OriginGestureStart): OriginGestureFromSelection {
|
||||
return Object.freeze({ to: toStage(Object.freeze(start)) });
|
||||
}
|
||||
|
||||
const fromEdge =
|
||||
(edge: OriginGestureEdge) => (distance: OriginGestureDistance) =>
|
||||
fromStage({ kind: "edge", edge, distance });
|
||||
|
||||
const fromBuilder: OriginGestureFromBuilder = Object.freeze({
|
||||
left: fromEdge("left"),
|
||||
right: fromEdge("right"),
|
||||
top: fromEdge("top"),
|
||||
bottom: fromEdge("bottom"),
|
||||
anywhere: () => fromStage({ kind: "anywhere" }),
|
||||
when: (predicate: OriginGestureStartPredicate) =>
|
||||
fromStage({ kind: "when", predicate }),
|
||||
});
|
||||
|
||||
/**
|
||||
* Root of the immutable gesture builder.
|
||||
*
|
||||
* `.from` is optional. Starting at `.to` admits pointer-down anywhere on the
|
||||
* bound host, exactly like `.from.anywhere().to`.
|
||||
*
|
||||
* @example Anywhere-to-right back gesture
|
||||
* ```ts
|
||||
* const swipeBack = gesture
|
||||
* .to.right()
|
||||
* .navigate((context) => context.canGoBack ? back() : null)
|
||||
* .animate(slideRight);
|
||||
* ```
|
||||
*
|
||||
* @example Predicate-gated gesture with custom completion
|
||||
* ```ts
|
||||
* const openPanel = gesture
|
||||
* .from.when(({ point, bounds }) => point.localX <= bounds.width * 0.08)
|
||||
* .to.down()
|
||||
* .complete(({ progress, velocity }) => progress > 0.5 || velocity > 1)
|
||||
* .navigate(() => above(originView(PanelView)))
|
||||
* .animate(dropPanel);
|
||||
* ```
|
||||
*/
|
||||
export const gesture: OriginGestureBuilder = Object.freeze({
|
||||
from: fromBuilder,
|
||||
to: toStage(Object.freeze({ kind: "anywhere" })),
|
||||
});
|
||||
Reference in New Issue
Block a user