From 4205802e70dc2b108b1de792d6cae0b671d5c74b Mon Sep 17 00:00:00 2001 From: Sig <62321214+sigprogramming@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:58:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[project-sequencer-statemachine]=20?= =?UTF-8?q?=E5=88=9D=E6=9C=9F=E3=82=B9=E3=83=86=E3=83=BC=E3=83=88=E3=81=AE?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E5=91=A8=E3=82=8A=E3=82=92=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=20(#2518)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba --- src/composables/useSequencerStateMachine.ts | 19 +++++--- src/sing/sequencerStateMachine/index.ts | 8 +++- src/sing/stateMachine.ts | 52 +++++++++++++++++++-- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/composables/useSequencerStateMachine.ts b/src/composables/useSequencerStateMachine.ts index 233db4acaa..75c0a4ad01 100644 --- a/src/composables/useSequencerStateMachine.ts +++ b/src/composables/useSequencerStateMachine.ts @@ -1,13 +1,17 @@ import { computed, ref } from "vue"; import { ComputedRefs, + IdleStateId, PartialStore, Refs, } from "@/sing/sequencerStateMachine/common"; import { getNoteDuration } from "@/sing/domain"; import { createSequencerStateMachine } from "@/sing/sequencerStateMachine"; -export const useSequencerStateMachine = (store: PartialStore) => { +export const useSequencerStateMachine = ( + store: PartialStore, + initialStateId: IdleStateId, +) => { const computedRefs: ComputedRefs = { snapTicks: computed(() => getNoteDuration(store.state.sequencerSnapType, store.state.tpqn), @@ -25,11 +29,14 @@ export const useSequencerStateMachine = (store: PartialStore) => { previewPitchEdit: ref(undefined), guideLineTicks: ref(0), }; - const stateMachine = createSequencerStateMachine({ - ...computedRefs, - ...refs, - store, - }); + const stateMachine = createSequencerStateMachine( + { + ...computedRefs, + ...refs, + store, + }, + initialStateId, + ); return { stateMachine, nowPreviewing: computed(() => refs.nowPreviewing.value), diff --git a/src/sing/sequencerStateMachine/index.ts b/src/sing/sequencerStateMachine/index.ts index 0450f5477a..d29ed53cf8 100644 --- a/src/sing/sequencerStateMachine/index.ts +++ b/src/sing/sequencerStateMachine/index.ts @@ -1,5 +1,6 @@ import { Context, + IdleStateId, Input, SequencerStateDefinitions, } from "@/sing/sequencerStateMachine/common"; @@ -17,7 +18,10 @@ import { SelectNotesWithRectState } from "@/sing/sequencerStateMachine/states/se import { DrawPitchState } from "@/sing/sequencerStateMachine/states/drawPitchState"; import { ErasePitchState } from "@/sing/sequencerStateMachine/states/erasePitchState"; -export const createSequencerStateMachine = (context: Context) => { +export const createSequencerStateMachine = ( + context: Context, + initialStateId: IdleStateId, +) => { return new StateMachine( { selectNotesToolIdle: () => new SelectNotesToolIdleState(), @@ -32,7 +36,7 @@ export const createSequencerStateMachine = (context: Context) => { drawPitch: (args) => new DrawPitchState(args), erasePitch: (args) => new ErasePitchState(args), }, - new SelectNotesToolIdleState(), context, + initialStateId, ); }; diff --git a/src/sing/stateMachine.ts b/src/sing/stateMachine.ts index e692a9c515..1411db58af 100644 --- a/src/sing/stateMachine.ts +++ b/src/sing/stateMachine.ts @@ -91,6 +91,15 @@ type StateFactories = { ) => State & { readonly id: U }; }; +/** + * 初期ステートとして設定可能なステートのIDを表す型。 + */ +type InitialStateId = T[number] extends infer U + ? U extends { id: string; factoryArgs: undefined } + ? U["id"] + : never + : never; + /** * ステートマシンを表すクラス。 * @@ -111,6 +120,7 @@ export class StateMachine< private readonly context: Context; private currentState: State; + private isDisposed = false; /** * ステートマシンの現在のステートのID。 @@ -120,19 +130,36 @@ export class StateMachine< } /** - * @param initialState ステートマシンの初期ステート。 - * @param context ステート間で共有されるコンテキスト。 + * @param stateFactories ステートのファクトリー関数。 + * @param context ステートマシンのコンテキスト。 + * @param initialStateId ステートマシンの初期ステートのID。 */ constructor( stateFactories: StateFactories, - initialState: State, context: Context, + initialStateId: InitialStateId, ) { this.stateFactories = stateFactories; this.context = context; - this.currentState = initialState; + this.currentState = stateFactories[initialStateId](undefined); + } + /** + * ステートを遷移し、ライフサイクルイベントを実行する。 + * + * @param id 遷移先のステートのID。 + * @param factoryArgs 遷移先のステートのファクトリー関数の引数。 + */ + transitionTo>( + id: T, + factoryArgs: FactoryArgs, + ) { + if (this.isDisposed) { + throw new Error("This state machine is already disposed."); + } + this.currentState.onExit(this.context); + this.currentState = this.stateFactories[id](factoryArgs); this.currentState.onEnter(this.context); } @@ -142,8 +169,13 @@ export class StateMachine< * @param input 処理する入力。 */ process(input: Input) { + if (this.isDisposed) { + throw new Error("This state machine is already disposed."); + } + let nextState: State | undefined = undefined; + this.currentState.process({ input, context: this.context, @@ -151,10 +183,22 @@ export class StateMachine< nextState = this.stateFactories[id](factoryArgs); }, }); + if (nextState != undefined) { this.currentState.onExit(this.context); this.currentState = nextState; this.currentState.onEnter(this.context); } } + + /** + * ステートマシンを破棄する。 + */ + dispose() { + if (this.isDisposed) { + throw new Error("Already disposed."); + } + this.isDisposed = true; + this.currentState.onExit(this.context); + } }