Skip to content

Commit

Permalink
refactor: [project-sequencer-statemachine] ファイルを分けて整理 (#2504)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigprogramming authored Jan 27, 2025
1 parent 2779fbe commit 8c26457
Show file tree
Hide file tree
Showing 13 changed files with 1,532 additions and 1,436 deletions.
42 changes: 42 additions & 0 deletions src/composables/useSequencerStateMachine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { computed, ref } from "vue";
import {
ComputedRefs,
PartialStore,
Refs,
} from "@/sing/sequencerStateMachine/common";
import { getNoteDuration } from "@/sing/domain";
import { createSequencerStateMachine } from "@/sing/sequencerStateMachine";

export const useSequencerStateMachine = (store: PartialStore) => {
const computedRefs: ComputedRefs = {
snapTicks: computed(() =>
getNoteDuration(store.state.sequencerSnapType, store.state.tpqn),
),
editTarget: computed(() => store.state.sequencerEditTarget),
selectedTrackId: computed(() => store.getters.SELECTED_TRACK_ID),
notesInSelectedTrack: computed(() => store.getters.SELECTED_TRACK.notes),
selectedNoteIds: computed(() => store.getters.SELECTED_NOTE_IDS),
editorFrameRate: computed(() => store.state.editorFrameRate),
};
const refs: Refs = {
nowPreviewing: ref(false),
previewNotes: ref([]),
previewRectForRectSelect: ref(undefined),
previewPitchEdit: ref(undefined),
guideLineTicks: ref(0),
};
const stateMachine = createSequencerStateMachine({
...computedRefs,
...refs,
store,
});
return {
stateMachine,
nowPreviewing: computed(() => refs.nowPreviewing.value),
previewNotes: computed(() => refs.previewNotes.value),
previewRectForRectSelect: computed(
() => refs.previewRectForRectSelect.value,
),
guideLineTicks: computed(() => refs.guideLineTicks.value),
};
};
222 changes: 222 additions & 0 deletions src/sing/sequencerStateMachine/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { ComputedRef, Ref } from "vue";
import { StateDefinitions } from "@/sing/stateMachine";
import { Rect } from "@/sing/utility";
import { PREVIEW_SOUND_DURATION } from "@/sing/viewHelper";
import { Store } from "@/store";
import { Note, SequencerEditTarget } from "@/store/type";
import { isOnCommandOrCtrlKeyDown } from "@/store/utility";
import { NoteId, TrackId } from "@/type/preload";

export type PositionOnSequencer = {
readonly x: number;
readonly y: number;
readonly ticks: number;
readonly noteNumber: number;
readonly frame: number;
readonly frequency: number;
};

export type Input =
| {
readonly targetArea: "SequencerBody";
readonly mouseEvent: MouseEvent;
readonly cursorPos: PositionOnSequencer;
}
| {
readonly targetArea: "Note";
readonly mouseEvent: MouseEvent;
readonly cursorPos: PositionOnSequencer;
readonly note: Note;
}
| {
readonly targetArea: "NoteLeftEdge";
readonly mouseEvent: MouseEvent;
readonly cursorPos: PositionOnSequencer;
readonly note: Note;
}
| {
readonly targetArea: "NoteRightEdge";
readonly mouseEvent: MouseEvent;
readonly cursorPos: PositionOnSequencer;
readonly note: Note;
};

export type ComputedRefs = {
readonly snapTicks: ComputedRef<number>;
readonly editTarget: ComputedRef<SequencerEditTarget>;
readonly selectedTrackId: ComputedRef<TrackId>;
readonly notesInSelectedTrack: ComputedRef<Note[]>;
readonly selectedNoteIds: ComputedRef<Set<NoteId>>;
readonly editorFrameRate: ComputedRef<number>;
};

export type Refs = {
readonly nowPreviewing: Ref<boolean>;
readonly previewNotes: Ref<Note[]>;
readonly previewRectForRectSelect: Ref<Rect | undefined>;
readonly previewPitchEdit: Ref<
| { type: "draw"; data: number[]; startFrame: number }
| { type: "erase"; startFrame: number; frameLength: number }
| undefined
>;
readonly guideLineTicks: Ref<number>;
};

export type PartialStore = {
state: Pick<
Store["state"],
"tpqn" | "sequencerSnapType" | "sequencerEditTarget" | "editorFrameRate"
>;
getters: Pick<
Store["getters"],
"SELECTED_TRACK_ID" | "SELECTED_TRACK" | "SELECTED_NOTE_IDS"
>;
actions: Pick<
Store["actions"],
| "SELECT_NOTES"
| "DESELECT_NOTES"
| "DESELECT_ALL_NOTES"
| "PLAY_PREVIEW_SOUND"
| "COMMAND_ADD_NOTES"
| "COMMAND_UPDATE_NOTES"
| "COMMAND_SET_PITCH_EDIT_DATA"
| "COMMAND_ERASE_PITCH_EDIT_DATA"
>;
};

export type Context = ComputedRefs & Refs & { readonly store: PartialStore };

export type SequencerStateDefinitions = StateDefinitions<
[
{
id: "idle";
factoryArgs: undefined;
},
{
id: "addNote";
factoryArgs: {
cursorPosAtStart: PositionOnSequencer;
targetTrackId: TrackId;
};
},
{
id: "moveNote";
factoryArgs: {
cursorPosAtStart: PositionOnSequencer;
targetTrackId: TrackId;
targetNoteIds: Set<NoteId>;
mouseDownNoteId: NoteId;
};
},
{
id: "resizeNoteLeft";
factoryArgs: {
cursorPosAtStart: PositionOnSequencer;
targetTrackId: TrackId;
targetNoteIds: Set<NoteId>;
mouseDownNoteId: NoteId;
};
},
{
id: "resizeNoteRight";
factoryArgs: {
cursorPosAtStart: PositionOnSequencer;
targetTrackId: TrackId;
targetNoteIds: Set<NoteId>;
mouseDownNoteId: NoteId;
};
},
{
id: "selectNotesWithRect";
factoryArgs: {
cursorPosAtStart: PositionOnSequencer;
};
},
{
id: "drawPitch";
factoryArgs: {
cursorPosAtStart: PositionOnSequencer;
targetTrackId: TrackId;
};
},
{
id: "erasePitch";
factoryArgs: {
cursorPosAtStart: PositionOnSequencer;
targetTrackId: TrackId;
};
},
]
>;

/**
* カーソル位置に対応する補助線の位置を取得する。
*/
export const getGuideLineTicks = (
cursorPos: PositionOnSequencer,
context: Context,
) => {
const cursorTicks = cursorPos.ticks;
const snapTicks = context.snapTicks.value;
// NOTE: 入力を補助する線の判定の境目はスナップ幅の3/4の位置
return Math.round(cursorTicks / snapTicks - 0.25) * snapTicks;
};

/**
* 指定されたノートのみを選択状態にする。
*/
export const selectOnlyThisNote = (context: Context, note: Note) => {
void context.store.actions.DESELECT_ALL_NOTES();
void context.store.actions.SELECT_NOTES({ noteIds: [note.id] });
};

/**
* mousedown時のノート選択・選択解除の処理を実行する。
*/
export const executeNotesSelectionProcess = (
context: Context,
mouseEvent: MouseEvent,
mouseDownNote: Note,
) => {
if (mouseEvent.shiftKey) {
// Shiftキーが押されている場合は選択ノートまでの範囲選択
let minIndex = context.notesInSelectedTrack.value.length - 1;
let maxIndex = 0;
for (let i = 0; i < context.notesInSelectedTrack.value.length; i++) {
const noteId = context.notesInSelectedTrack.value[i].id;
if (
context.selectedNoteIds.value.has(noteId) ||
noteId === mouseDownNote.id
) {
minIndex = Math.min(minIndex, i);
maxIndex = Math.max(maxIndex, i);
}
}
const noteIdsToSelect: NoteId[] = [];
for (let i = minIndex; i <= maxIndex; i++) {
const noteId = context.notesInSelectedTrack.value[i].id;
if (!context.selectedNoteIds.value.has(noteId)) {
noteIdsToSelect.push(noteId);
}
}
void context.store.actions.SELECT_NOTES({ noteIds: noteIdsToSelect });
} else if (isOnCommandOrCtrlKeyDown(mouseEvent)) {
// CommandキーかCtrlキーが押されている場合
if (context.selectedNoteIds.value.has(mouseDownNote.id)) {
// 選択中のノートなら選択解除
void context.store.actions.DESELECT_NOTES({
noteIds: [mouseDownNote.id],
});
return;
}
// 未選択のノートなら選択に追加
void context.store.actions.SELECT_NOTES({ noteIds: [mouseDownNote.id] });
} else if (!context.selectedNoteIds.value.has(mouseDownNote.id)) {
// 選択中のノートでない場合は選択状態にする
void selectOnlyThisNote(context, mouseDownNote);
void context.store.actions.PLAY_PREVIEW_SOUND({
noteNumber: mouseDownNote.noteNumber,
duration: PREVIEW_SOUND_DURATION,
});
}
};
32 changes: 32 additions & 0 deletions src/sing/sequencerStateMachine/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
Context,
Input,
SequencerStateDefinitions,
} from "@/sing/sequencerStateMachine/common";
import { StateMachine } from "@/sing/stateMachine";

import { IdleState } from "@/sing/sequencerStateMachine/states/idleState";
import { AddNoteState } from "@/sing/sequencerStateMachine/states/addNoteState";
import { MoveNoteState } from "@/sing/sequencerStateMachine/states/moveNoteState";
import { ResizeNoteLeftState } from "@/sing/sequencerStateMachine/states/resizeNoteLeftState";
import { ResizeNoteRightState } from "@/sing/sequencerStateMachine/states/resizeNoteRightState";
import { SelectNotesWithRectState } from "@/sing/sequencerStateMachine/states/selectNotesWithRectState";
import { DrawPitchState } from "@/sing/sequencerStateMachine/states/drawPitchState";
import { ErasePitchState } from "@/sing/sequencerStateMachine/states/erasePitchState";

export const createSequencerStateMachine = (context: Context) => {
return new StateMachine<SequencerStateDefinitions, Input, Context>(
{
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(),
context,
);
};
Loading

0 comments on commit 8c26457

Please sign in to comment.