@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
121 lines (103 loc) • 3.78 kB
text/typescript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
export type TransitionCallback<S, T> = (params: {
/** The object that this transition applies to. */
value: T;
/** The starting state of the transition. */
from: S;
/** The end state of the transition. */
to: S;
}) => void;
export type Transition<T> = [T, T];
export type State = number | string;
/**
* A simple state machine that supports a list of legal transitions, as well as callbacks
* to call before and after each transition.
*
* Note: self-transitions are implicitly legal, so they don't have to be specified in the list of
* legal transitions.
*/
export default class StateMachine<S extends State, T extends { state: S }> {
private readonly _legalTransitions: Map<S, Transition<S>[]> = new Map();
private readonly _postTransitionCallbacks: Map<S, TransitionCallback<S, T>> = new Map();
private readonly _preTransitionCallbacks: Map<S, TransitionCallback<S, T>> = new Map();
public constructor(params: {
/**
* The list of legal transitions.
*/
legalTransitions: Transition<S>[];
}) {
for (const transition of params.legalTransitions) {
const [from] = transition;
if (!this._legalTransitions.has(from)) {
this._legalTransitions.set(from, []);
}
this._legalTransitions.get(from)?.push(transition);
}
}
/**
* Registers a callback that will be called on each object
* that makes a transition to {@link endState}.
*/
public addPostTransitionCallback(endState: S, callback: TransitionCallback<S, T>): void {
this._postTransitionCallbacks.set(endState, callback);
}
/**
* Registers a callback that will be called on each object
* that makes a transition from {@link fromState}.
*/
public addPreTransitionCallback(fromState: S, callback: TransitionCallback<S, T>): void {
this._preTransitionCallbacks.set(fromState, callback);
}
public isTransitionLegal(from: S, to: S): boolean {
const entry = this._legalTransitions.get(from);
if (entry) {
return entry.some(([, transitionTo]) => transitionTo === to);
}
return false;
}
/**
* Transition the object from its current state to the specified target state, applying
* relevant pre-transition and post-transition callbacks on the object.
*
* If the start and end states are the same, nothing happens,
* unless `allowSelftransition` is true.
*
* @throws If the transition is illegal, throws an error.
*
* @returns True if a transition happened, false otherwise.
*/
public transition(
obj: T,
to: S,
options?: {
/** Allow transitioning from the same starting and end state. */
allowSelfTransition?: boolean;
},
): boolean {
const from = obj.state;
const allowSelfTransition = options?.allowSelfTransition ?? false;
if (from === to) {
if (!allowSelfTransition) {
return false;
}
} else {
if (!this.isTransitionLegal(from, to)) {
throw new Error(`illegal transition: ${from} -> ${to}`);
}
}
const preTransition = this._preTransitionCallbacks.get(from);
if (preTransition) {
preTransition({ value: obj, from, to });
}
obj.state = to;
const postTransition = this._postTransitionCallbacks.get(to);
if (postTransition) {
postTransition({ value: obj, from, to });
}
return true;
}
}