From ee14b01da962ec118d51704044d83062e155baef Mon Sep 17 00:00:00 2001 From: Sig <62321214+sigprogramming@users.noreply.github.com> Date: Wed, 22 Jan 2025 21:40:44 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=BC=E3=83=88=E3=83=9E?= =?UTF-8?q?=E3=82=B7=E3=83=B3=E3=81=AE=E5=9E=8B=E5=AE=9A=E7=BE=A9=E3=82=92?= =?UTF-8?q?=E5=BC=B7=E5=8C=96=E3=81=97=E3=80=81=E3=83=95=E3=82=A1=E3=82=AF?= =?UTF-8?q?=E3=83=88=E3=83=AA=E9=96=A2=E6=95=B0=E3=81=AE=E5=BC=95=E6=95=B0?= =?UTF-8?q?=E3=82=92=E5=9E=8B=E5=AE=89=E5=85=A8=E3=81=AB=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stateMachine/sequencerStateMachine.ts | 319 +++++++++++------- src/sing/stateMachine/stateMachineBase.ts | 102 ++++-- 2 files changed, 277 insertions(+), 144 deletions(-) diff --git a/src/sing/stateMachine/sequencerStateMachine.ts b/src/sing/stateMachine/sequencerStateMachine.ts index cb132e2ac9..87c4e52b02 100644 --- a/src/sing/stateMachine/sequencerStateMachine.ts +++ b/src/sing/stateMachine/sequencerStateMachine.ts @@ -11,7 +11,11 @@ import { linearInterpolation, Rect, } from "@/sing/utility"; -import { IState, StateMachine } from "@/sing/stateMachine/stateMachineBase"; +import { + IState, + SetNextStateFunc, + StateMachine, +} from "@/sing/stateMachine/stateMachineBase"; import { getButton, getDoremiFromNoteNumber, @@ -122,15 +126,66 @@ type PartialStore = { type Context = ComputedRefs & Refs & { readonly store: PartialStore }; -type State = - | IdleState - | AddNoteState - | MoveNoteState - | ResizeNoteLeftState - | ResizeNoteRightState - | SelectNotesWithRectState - | DrawPitchState - | ErasePitchState; +type StateDefinitions = [ + { + id: "idle"; + factoryFuncArgs: undefined; + }, + { + id: "addNote"; + factoryFuncArgs: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + }; + }, + { + id: "moveNote"; + factoryFuncArgs: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + targetNoteIds: Set; + mouseDownNoteId: NoteId; + }; + }, + { + id: "resizeNoteLeft"; + factoryFuncArgs: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + targetNoteIds: Set; + mouseDownNoteId: NoteId; + }; + }, + { + id: "resizeNoteRight"; + factoryFuncArgs: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + targetNoteIds: Set; + mouseDownNoteId: NoteId; + }; + }, + { + id: "selectNotesWithRect"; + factoryFuncArgs: { + cursorPosAtStart: PositionOnSequencer; + }; + }, + { + id: "drawPitch"; + factoryFuncArgs: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + }; + }, + { + id: "erasePitch"; + factoryFuncArgs: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + }; + }, +]; const getGuideLineTicks = ( cursorPos: PositionOnSequencer, @@ -198,7 +253,7 @@ const executeNotesSelectionProcess = ( } }; -class IdleState implements IState { +class IdleState implements IState { readonly id = "idle"; onEnter() {} @@ -210,7 +265,7 @@ class IdleState implements IState { }: { input: Input; context: Context; - setNextState: (nextState: State) => void; + setNextState: SetNextStateFunc; }) { const mouseButton = getButton(input.mouseEvent); const selectedTrackId = context.selectedTrackId.value; @@ -229,45 +284,40 @@ class IdleState implements IState { ) { if (input.targetArea === "SequencerBody") { if (input.mouseEvent.shiftKey) { - const selectNotesWithRectState = new SelectNotesWithRectState( - input.cursorPos, - ); - setNextState(selectNotesWithRectState); + setNextState("selectNotesWithRect", { + cursorPosAtStart: input.cursorPos, + }); } else { void context.store.actions.DESELECT_ALL_NOTES(); - const addNoteState = new AddNoteState( - input.cursorPos, - selectedTrackId, - ); - setNextState(addNoteState); + setNextState("addNote", { + cursorPosAtStart: input.cursorPos, + targetTrackId: selectedTrackId, + }); } } else if (input.targetArea === "Note") { executeNotesSelectionProcess(context, input.mouseEvent, input.note); - const moveNoteState = new MoveNoteState( - input.cursorPos, - selectedTrackId, - context.selectedNoteIds.value, - input.note.id, - ); - setNextState(moveNoteState); + setNextState("moveNote", { + cursorPosAtStart: input.cursorPos, + targetTrackId: selectedTrackId, + targetNoteIds: context.selectedNoteIds.value, + mouseDownNoteId: input.note.id, + }); } else if (input.targetArea === "NoteLeftEdge") { executeNotesSelectionProcess(context, input.mouseEvent, input.note); - const moveNoteState = new ResizeNoteLeftState( - input.cursorPos, - selectedTrackId, - context.selectedNoteIds.value, - input.note.id, - ); - setNextState(moveNoteState); + setNextState("resizeNoteLeft", { + cursorPosAtStart: input.cursorPos, + targetTrackId: selectedTrackId, + targetNoteIds: context.selectedNoteIds.value, + mouseDownNoteId: input.note.id, + }); } else if (input.targetArea === "NoteRightEdge") { executeNotesSelectionProcess(context, input.mouseEvent, input.note); - const moveNoteState = new ResizeNoteRightState( - input.cursorPos, - selectedTrackId, - context.selectedNoteIds.value, - input.note.id, - ); - setNextState(moveNoteState); + setNextState("resizeNoteRight", { + cursorPosAtStart: input.cursorPos, + targetTrackId: selectedTrackId, + targetNoteIds: context.selectedNoteIds.value, + mouseDownNoteId: input.note.id, + }); } } } else if (context.editTarget.value === "PITCH") { @@ -279,17 +329,15 @@ class IdleState implements IState { // TODO: Ctrlが押されているときではなく、 // ピッチ削除ツールのときにErasePitchStateに遷移するようにする if (isOnCommandOrCtrlKeyDown(input.mouseEvent)) { - const erasePitchState = new ErasePitchState( - input.cursorPos, - selectedTrackId, - ); - setNextState(erasePitchState); + setNextState("erasePitch", { + cursorPosAtStart: input.cursorPos, + targetTrackId: selectedTrackId, + }); } else { - const drawPitchState = new DrawPitchState( - input.cursorPos, - selectedTrackId, - ); - setNextState(drawPitchState); + setNextState("drawPitch", { + cursorPosAtStart: input.cursorPos, + targetTrackId: selectedTrackId, + }); } } } @@ -298,7 +346,7 @@ class IdleState implements IState { onExit() {} } -class AddNoteState implements IState { +class AddNoteState implements IState { readonly id = "addNote"; private readonly cursorPosAtStart: PositionOnSequencer; @@ -313,11 +361,14 @@ class AddNoteState implements IState { } | undefined; - constructor(cursorPosAtStart: PositionOnSequencer, targetTrackId: TrackId) { - this.cursorPosAtStart = cursorPosAtStart; - this.targetTrackId = targetTrackId; + constructor(args: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + }) { + this.cursorPosAtStart = args.cursorPosAtStart; + this.targetTrackId = args.targetTrackId; - this.currentCursorPos = cursorPosAtStart; + this.currentCursorPos = args.cursorPosAtStart; } private previewAdd(context: Context) { @@ -385,7 +436,7 @@ class AddNoteState implements IState { }: { input: Input; context: Context; - setNextState: (nextState: State) => void; + setNextState: SetNextStateFunc; }) { if (this.innerContext == undefined) { throw new Error("innerContext is undefined."); @@ -397,7 +448,7 @@ class AddNoteState implements IState { this.innerContext.executePreviewProcess = true; } else if (input.mouseEvent.type === "mouseup") { if (mouseButton === "LEFT_BUTTON") { - setNextState(new IdleState()); + setNextState("idle", undefined); } } } @@ -429,7 +480,7 @@ class AddNoteState implements IState { } } -class MoveNoteState implements IState { +class MoveNoteState implements IState { readonly id = "moveNote"; private readonly cursorPosAtStart: PositionOnSequencer; @@ -449,21 +500,21 @@ class MoveNoteState implements IState { } | undefined; - constructor( - cursorPosAtStart: PositionOnSequencer, - targetTrackId: TrackId, - targetNoteIds: Set, - mouseDownNoteId: NoteId, - ) { - if (!targetNoteIds.has(mouseDownNoteId)) { + constructor(args: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + targetNoteIds: Set; + mouseDownNoteId: NoteId; + }) { + if (!args.targetNoteIds.has(args.mouseDownNoteId)) { throw new Error("mouseDownNoteId is not included in targetNoteIds."); } - this.cursorPosAtStart = cursorPosAtStart; - this.targetTrackId = targetTrackId; - this.targetNoteIds = targetNoteIds; - this.mouseDownNoteId = mouseDownNoteId; + this.cursorPosAtStart = args.cursorPosAtStart; + this.targetTrackId = args.targetTrackId; + this.targetNoteIds = args.targetNoteIds; + this.mouseDownNoteId = args.mouseDownNoteId; - this.currentCursorPos = cursorPosAtStart; + this.currentCursorPos = args.cursorPosAtStart; } private previewMove(context: Context) { @@ -549,7 +600,7 @@ class MoveNoteState implements IState { }: { input: Input; context: Context; - setNextState: (nextState: State) => void; + setNextState: SetNextStateFunc; }) { if (this.innerContext == undefined) { throw new Error("innerContext is undefined."); @@ -561,7 +612,7 @@ class MoveNoteState implements IState { this.innerContext.executePreviewProcess = true; } else if (input.mouseEvent.type === "mouseup") { if (mouseButton === "LEFT_BUTTON") { - setNextState(new IdleState()); + setNextState("idle", undefined); } } } @@ -595,7 +646,7 @@ class MoveNoteState implements IState { } } -class ResizeNoteLeftState implements IState { +class ResizeNoteLeftState implements IState { readonly id = "resizeNoteLeft"; private readonly cursorPosAtStart: PositionOnSequencer; @@ -615,21 +666,21 @@ class ResizeNoteLeftState implements IState { } | undefined; - constructor( - cursorPosAtStart: PositionOnSequencer, - targetTrackId: TrackId, - targetNoteIds: Set, - mouseDownNoteId: NoteId, - ) { - if (!targetNoteIds.has(mouseDownNoteId)) { + constructor(args: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + targetNoteIds: Set; + mouseDownNoteId: NoteId; + }) { + if (!args.targetNoteIds.has(args.mouseDownNoteId)) { throw new Error("mouseDownNoteId is not included in targetNoteIds."); } - this.cursorPosAtStart = cursorPosAtStart; - this.targetTrackId = targetTrackId; - this.targetNoteIds = targetNoteIds; - this.mouseDownNoteId = mouseDownNoteId; + this.cursorPosAtStart = args.cursorPosAtStart; + this.targetTrackId = args.targetTrackId; + this.targetNoteIds = args.targetNoteIds; + this.mouseDownNoteId = args.mouseDownNoteId; - this.currentCursorPos = cursorPosAtStart; + this.currentCursorPos = args.cursorPosAtStart; } private previewResizeLeft(context: Context) { @@ -710,7 +761,7 @@ class ResizeNoteLeftState implements IState { }: { input: Input; context: Context; - setNextState: (nextState: State) => void; + setNextState: SetNextStateFunc; }) { if (this.innerContext == undefined) { throw new Error("innerContext is undefined."); @@ -724,7 +775,7 @@ class ResizeNoteLeftState implements IState { input.mouseEvent.type === "mouseup" && mouseButton === "LEFT_BUTTON" ) { - setNextState(new IdleState()); + setNextState("idle", undefined); } } } @@ -755,7 +806,7 @@ class ResizeNoteLeftState implements IState { } } -class ResizeNoteRightState implements IState { +class ResizeNoteRightState implements IState { readonly id = "resizeNoteRight"; private readonly cursorPosAtStart: PositionOnSequencer; @@ -775,21 +826,21 @@ class ResizeNoteRightState implements IState { } | undefined; - constructor( - cursorPosAtStart: PositionOnSequencer, - targetTrackId: TrackId, - targetNoteIds: Set, - mouseDownNoteId: NoteId, - ) { - if (!targetNoteIds.has(mouseDownNoteId)) { + constructor(args: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + targetNoteIds: Set; + mouseDownNoteId: NoteId; + }) { + if (!args.targetNoteIds.has(args.mouseDownNoteId)) { throw new Error("mouseDownNoteId is not included in targetNoteIds."); } - this.cursorPosAtStart = cursorPosAtStart; - this.targetTrackId = targetTrackId; - this.targetNoteIds = targetNoteIds; - this.mouseDownNoteId = mouseDownNoteId; + this.cursorPosAtStart = args.cursorPosAtStart; + this.targetTrackId = args.targetTrackId; + this.targetNoteIds = args.targetNoteIds; + this.mouseDownNoteId = args.mouseDownNoteId; - this.currentCursorPos = cursorPosAtStart; + this.currentCursorPos = args.cursorPosAtStart; } private previewResizeRight(context: Context) { @@ -869,7 +920,7 @@ class ResizeNoteRightState implements IState { }: { input: Input; context: Context; - setNextState: (nextState: State) => void; + setNextState: SetNextStateFunc; }) { if (this.innerContext == undefined) { throw new Error("innerContext is undefined."); @@ -883,7 +934,7 @@ class ResizeNoteRightState implements IState { input.mouseEvent.type === "mouseup" && mouseButton === "LEFT_BUTTON" ) { - setNextState(new IdleState()); + setNextState("idle", undefined); } } } @@ -916,7 +967,9 @@ class ResizeNoteRightState implements IState { } } -class SelectNotesWithRectState implements IState { +class SelectNotesWithRectState + implements IState +{ readonly id = "selectNotesWithRect"; private readonly cursorPosAtStart: PositionOnSequencer; @@ -924,10 +977,10 @@ class SelectNotesWithRectState implements IState { private currentCursorPos: PositionOnSequencer; private additive: boolean; - constructor(cursorPosAtStart: PositionOnSequencer) { - this.cursorPosAtStart = cursorPosAtStart; + constructor(args: { cursorPosAtStart: PositionOnSequencer }) { + this.cursorPosAtStart = args.cursorPosAtStart; - this.currentCursorPos = cursorPosAtStart; + this.currentCursorPos = args.cursorPosAtStart; this.additive = false; } @@ -956,7 +1009,7 @@ class SelectNotesWithRectState implements IState { }: { input: Input; context: Context; - setNextState: (nextState: State) => void; + setNextState: SetNextStateFunc; }) { const mouseButton = getButton(input.mouseEvent); if (input.targetArea === "SequencerBody") { @@ -968,7 +1021,7 @@ class SelectNotesWithRectState implements IState { mouseButton === "LEFT_BUTTON" ) { this.additive = isOnCommandOrCtrlKeyDown(input.mouseEvent); - setNextState(new IdleState()); + setNextState("idle", undefined); } } } @@ -1011,7 +1064,7 @@ class SelectNotesWithRectState implements IState { } } -class DrawPitchState implements IState { +class DrawPitchState implements IState { readonly id = "drawPitch"; private readonly cursorPosAtStart: PositionOnSequencer; @@ -1027,11 +1080,14 @@ class DrawPitchState implements IState { } | undefined; - constructor(cursorPosAtStart: PositionOnSequencer, targetTrackId: TrackId) { - this.cursorPosAtStart = cursorPosAtStart; - this.targetTrackId = targetTrackId; + constructor(args: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + }) { + this.cursorPosAtStart = args.cursorPosAtStart; + this.targetTrackId = args.targetTrackId; - this.currentCursorPos = cursorPosAtStart; + this.currentCursorPos = args.cursorPosAtStart; } private previewDrawPitch(context: Context) { @@ -1139,7 +1195,7 @@ class DrawPitchState implements IState { }: { input: Input; context: Context; - setNextState: (nextState: State) => void; + setNextState: SetNextStateFunc; }) { if (this.innerContext == undefined) { throw new Error("innerContext is undefined."); @@ -1151,7 +1207,7 @@ class DrawPitchState implements IState { this.innerContext.executePreviewProcess = true; } else if (input.mouseEvent.type === "mouseup") { if (mouseButton === "LEFT_BUTTON") { - setNextState(new IdleState()); + setNextState("idle", undefined); } } } @@ -1191,7 +1247,7 @@ class DrawPitchState implements IState { } } -class ErasePitchState implements IState { +class ErasePitchState implements IState { readonly id = "erasePitch"; private readonly cursorPosAtStart: PositionOnSequencer; @@ -1206,11 +1262,14 @@ class ErasePitchState implements IState { } | undefined; - constructor(cursorPosAtStart: PositionOnSequencer, targetTrackId: TrackId) { - this.cursorPosAtStart = cursorPosAtStart; - this.targetTrackId = targetTrackId; + constructor(args: { + cursorPosAtStart: PositionOnSequencer; + targetTrackId: TrackId; + }) { + this.cursorPosAtStart = args.cursorPosAtStart; + this.targetTrackId = args.targetTrackId; - this.currentCursorPos = cursorPosAtStart; + this.currentCursorPos = args.cursorPosAtStart; } private previewErasePitch(context: Context) { @@ -1272,7 +1331,7 @@ class ErasePitchState implements IState { }: { input: Input; context: Context; - setNextState: (nextState: State) => void; + setNextState: SetNextStateFunc; }) { if (this.innerContext == undefined) { throw new Error("innerContext is undefined."); @@ -1284,7 +1343,7 @@ class ErasePitchState implements IState { this.innerContext.executePreviewProcess = true; } else if (input.mouseEvent.type === "mouseup") { if (mouseButton === "LEFT_BUTTON") { - setNextState(new IdleState()); + setNextState("idle", undefined); } } } @@ -1332,7 +1391,17 @@ export const useSequencerStateMachine = (store: PartialStore) => { previewPitchEdit: ref(undefined), guideLineTicks: ref(0), }; - const stateMachine = new StateMachine( + const stateMachine = new StateMachine( + { + idle: () => new IdleState(), + addNote: (args) => new AddNoteState(args), + moveNote: (args) => new MoveNoteState(args), + resizeNoteLeft: (args) => new ResizeNoteLeftState(args), + resizeNoteRight: (args) => new ResizeNoteRightState(args), + selectNotesWithRect: (args) => new SelectNotesWithRectState(args), + drawPitch: (args) => new DrawPitchState(args), + erasePitch: (args) => new ErasePitchState(args), + }, new IdleState(), { ...computedRefs, diff --git a/src/sing/stateMachine/stateMachineBase.ts b/src/sing/stateMachine/stateMachineBase.ts index 6d6c5a09ac..a12f4a590d 100644 --- a/src/sing/stateMachine/stateMachineBase.ts +++ b/src/sing/stateMachine/stateMachineBase.ts @@ -3,6 +3,39 @@ * issue: https://github.com/VOICEVOX/voicevox/issues/2041 */ +/** + * 指定されたIDを持つ型を抽出するユーティリティ型。 + */ +type ExtractById = T extends { id: U } ? T : never; + +/** + * ステートの定義を表す型。 + */ +type StateDefinition = { id: string; factoryFuncArgs: object | undefined }; + +/** + * ステートのIDを表す型。 + */ +type StateId = T[number]["id"]; + +/** + * ファクトリ関数の引数を表す型。 + */ +type FactoryFuncArgs< + T extends StateDefinition[], + U extends StateId, +> = ExtractById["factoryFuncArgs"]; + +/** + * 次のステートを設定する関数の型。 + */ +export type SetNextStateFunc = < + U extends StateId, +>( + id: U, + factoryFuncArgs: FactoryFuncArgs, +) => void; + /** * ステートマシンのステートを表すインターフェース。 * @@ -11,10 +44,12 @@ * @template Context ステート間で共有されるコンテキストの型。 */ export interface IState< - State extends IState, + StateDefinitions extends StateDefinition[], Input, Context, > { + readonly id: StateId; + /** * 入力を処理し、必要に応じて次のステートを設定する。 * @@ -23,7 +58,7 @@ export interface IState< process(payload: { input: Input; context: Context; - setNextState: (nextState: State) => void; + setNextState: SetNextStateFunc; }): void; /** @@ -41,6 +76,20 @@ export interface IState< onExit(context: Context): void; } +/** + * ステートのファクトリ関数を表す型。 + */ +type StateFactories< + T extends StateDefinition[], + U extends StateId, + Input, + Context, +> = { + [P in U]: ( + args: FactoryFuncArgs, + ) => IState & { readonly id: P }; +}; + /** * ステートマシンを表すクラス。 * @@ -49,48 +98,63 @@ export interface IState< * @template Context ステート間で共有されるコンテキストの型。 */ export class StateMachine< - State extends IState, + StateDefinitions extends StateDefinition[], Input, Context, > { + private readonly stateFactories: StateFactories< + StateDefinitions, + StateId, + Input, + Context + >; private readonly context: Context; - private currentState: State; + private currentState: IState; + + /** + * ステートマシンの現在のステートのID。 + */ + get currentStateId() { + return this.currentState.id; + } /** * @param initialState ステートマシンの初期ステート。 * @param context ステート間で共有されるコンテキスト。 */ - constructor(initialState: State, context: Context) { + constructor( + stateFactories: StateFactories< + StateDefinitions, + StateId, + Input, + Context + >, + initialState: IState, + context: Context, + ) { + this.stateFactories = stateFactories; this.context = context; + this.currentState = initialState; this.currentState.onEnter(this.context); } - /** - * ステートマシンの現在のステートを返す。 - * - * @returns 現在のステート。 - */ - getCurrentState() { - return this.currentState; - } - /** * 現在のステートを使用して入力を処理し、必要に応じてステートの遷移を行う。 * * @param input 処理する入力。 */ process(input: Input) { - let nextState: State | undefined = undefined; - const setNextState = (arg: State) => { - nextState = arg; - }; + let nextState: IState | undefined = + undefined; this.currentState.process({ input, context: this.context, - setNextState, + setNextState: (id, factoryFuncArgs) => { + nextState = this.stateFactories[id](factoryFuncArgs); + }, }); if (nextState != undefined) { this.currentState.onExit(this.context);