Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[project-sequencer-statemachine] ファイルを分けて整理 #2504

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading