Skip to content

Commit

Permalink
MoveNoteStateを追加
Browse files Browse the repository at this point in the history
  • Loading branch information
sigprogramming committed Dec 11, 2024
1 parent fecba4e commit d37de3c
Showing 1 changed file with 244 additions and 16 deletions.
260 changes: 244 additions & 16 deletions src/sing/stateMachine/sequencerStateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import {
getButton,
getDoremiFromNoteNumber,
isSelfEventTarget,
keyInfos,
PREVIEW_SOUND_DURATION,
} from "@/sing/viewHelper";
import { Note, SequencerEditTarget } from "@/store/type";
import { NoteId } from "@/type/preload";
import { NoteId, TrackId } from "@/type/preload";
import { getOrThrow } from "@/helpers/mapHelper";
import { isOnCommandOrCtrlKeyDown } from "@/store/utility";

export type PositionOnSequencer = {
readonly ticks: number;
Expand Down Expand Up @@ -47,6 +50,9 @@ type Input =
type ComputedRefs = {
readonly snapTicks: ComputedRef<number>;
readonly editTarget: ComputedRef<SequencerEditTarget>;
readonly selectedTrackId: ComputedRef<TrackId>;
readonly notesInSelectedTrack: ComputedRef<Note[]>;
readonly selectedNoteIds: ComputedRef<Set<NoteId>>;
};

type Refs = {
Expand All @@ -57,14 +63,16 @@ type Refs = {

type StoreActions = {
readonly deselectAllNotes: () => void;
readonly commandAddNotes: (notes: Note[]) => void;
readonly deselectNotes: (noteIds: NoteId[]) => void;
readonly commandAddNotes: (notes: Note[], trackId: TrackId) => void;
readonly commandUpdateNotes: (notes: Note[], trackId: TrackId) => void;
readonly selectNotes: (noteIds: NoteId[]) => void;
readonly playPreviewSound: (noteNumber: number, duration?: number) => void;
};

type Context = ComputedRefs & Refs & { readonly storeActions: StoreActions };

type State = IdleState | AddNoteState;
type State = IdleState | AddNoteState | MoveNoteState;

const getGuideLineTicks = (
cursorPos: PositionOnSequencer,
Expand All @@ -76,9 +84,54 @@ const getGuideLineTicks = (
return Math.round(cursorTicks / snapTicks - 0.25) * snapTicks;
};

const selectOnlyThisNote = (context: Context, note: Note) => {
void context.storeActions.deselectAllNotes();
void context.storeActions.selectNotes([note.id]);
void context.storeActions.playPreviewSound(note.noteNumber, PREVIEW_SOUND_DURATION);

Check failure on line 90 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `note.noteNumber,·PREVIEW_SOUND_DURATION` with `⏎····note.noteNumber,⏎····PREVIEW_SOUND_DURATION,⏎··`

Check failure on line 90 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / build-test

Replace `note.noteNumber,·PREVIEW_SOUND_DURATION` with `⏎····note.noteNumber,⏎····PREVIEW_SOUND_DURATION,⏎··`
};

class IdleState implements IState<State, Input, Context> {
readonly id = "idle";

private executeNotesSelectionProcess(context: Context, mouseEvent: MouseEvent, mouseDownNote: Note) {

Check failure on line 96 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `context:·Context,·mouseEvent:·MouseEvent,·mouseDownNote:·Note` with `⏎····context:·Context,⏎····mouseEvent:·MouseEvent,⏎····mouseDownNote:·Note,⏎··`

Check failure on line 96 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / build-test

Replace `context:·Context,·mouseEvent:·MouseEvent,·mouseDownNote:·Note` with `⏎····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) {

Check failure on line 103 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `context.selectedNoteIds.value.has(noteId)·||·noteId·===·mouseDownNote.id` with `⏎··········context.selectedNoteIds.value.has(noteId)·||⏎··········noteId·===·mouseDownNote.id⏎········`

Check failure on line 103 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / build-test

Replace `context.selectedNoteIds.value.has(noteId)·||·noteId·===·mouseDownNote.id` with `⏎··········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.storeActions.selectNotes(noteIdsToSelect);
} else if (isOnCommandOrCtrlKeyDown(mouseEvent)) {
// CommandキーかCtrlキーが押されている場合
if (context.selectedNoteIds.value.has(mouseDownNote.id)) {
// 選択中のノートなら選択解除
void context.storeActions.deselectNotes([mouseDownNote.id]);
return;
}
// 未選択のノートなら選択に追加
void context.storeActions.selectNotes([mouseDownNote.id]);
} else if (!context.selectedNoteIds.value.has(mouseDownNote.id)) {
// 選択中のノートでない場合は選択状態にする
void selectOnlyThisNote(context, mouseDownNote);
}
}

private getSelectedNotes(context: Context) {
return context.notesInSelectedTrack.value.filter(value => context.selectedNoteIds.value.has(value.id));

Check failure on line 132 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `value·=>·context.selectedNoteIds.value.has(value.id)` with `(value)·=>⏎······context.selectedNoteIds.value.has(value.id),⏎····`

Check failure on line 132 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / build-test

Replace `value·=>·context.selectedNoteIds.value.has(value.id)` with `(value)·=>⏎······context.selectedNoteIds.value.has(value.id),⏎····`
}

onEnter() {}

process({
Expand All @@ -91,14 +144,15 @@ class IdleState implements IState<State, Input, Context> {
setNextState: (nextState: State) => void;
}) {
const mouseButton = getButton(input.mouseEvent);
if (input.targetArea === "SequencerBody") {
context.guideLineTicks.value = getGuideLineTicks(
input.cursorPos,
context,
);
if (input.mouseEvent.type === "mousedown") {
// TODO: メニューが表示されている場合はメニュー非表示のみ行いたい
if (context.editTarget.value === "NOTE") {
const selectedTrackId = context.selectedTrackId.value;

if (context.editTarget.value === "NOTE") {
if (input.targetArea === "SequencerBody") {
context.guideLineTicks.value = getGuideLineTicks(
input.cursorPos,
context,
);
if (input.mouseEvent.type === "mousedown") {
if (!isSelfEventTarget(input.mouseEvent)) {
return;
}
Expand All @@ -110,11 +164,27 @@ class IdleState implements IState<State, Input, Context> {
) {
return;
}
setNextState(new AddNoteState(input.cursorPos));
context.storeActions.deselectAllNotes();
const addNoteState = new AddNoteState(input.cursorPos, selectedTrackId);

Check failure on line 168 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `input.cursorPos,·selectedTrackId` with `⏎··············input.cursorPos,⏎··············selectedTrackId,⏎············`

Check failure on line 168 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / build-test

Replace `input.cursorPos,·selectedTrackId` with `⏎··············input.cursorPos,⏎··············selectedTrackId,⏎············`
setNextState(addNoteState);
}
}
} else if (input.targetArea === "Note") {
if (input.mouseEvent.type === "mousedown") {
if (!isSelfEventTarget(input.mouseEvent)) {
return;
}
if (mouseButton === "LEFT_BUTTON") {
this.executeNotesSelectionProcess(context, input.mouseEvent, input.note);

Check failure on line 178 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `context,·input.mouseEvent,·input.note` with `⏎··············context,⏎··············input.mouseEvent,⏎··············input.note,⏎············`

Check failure on line 178 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / build-test

Replace `context,·input.mouseEvent,·input.note` with `⏎··············context,⏎··············input.mouseEvent,⏎··············input.note,⏎············`
const selectedNotes = this.getSelectedNotes(context);
const moveNoteState = new MoveNoteState(input.cursorPos, selectedTrackId, selectedNotes, input.note.id);

Check failure on line 180 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `input.cursorPos,·selectedTrackId,·selectedNotes,·input.note.id` with `⏎··············input.cursorPos,⏎··············selectedTrackId,⏎··············selectedNotes,⏎··············input.note.id,⏎············`

Check failure on line 180 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / build-test

Replace `input.cursorPos,·selectedTrackId,·selectedNotes,·input.note.id` with `⏎··············input.cursorPos,⏎··············selectedTrackId,⏎··············selectedNotes,⏎··············input.note.id,⏎············`
setNextState(moveNoteState);
}
}
}
}

Check failure on line 185 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / lint

Delete `⏎⏎····`

Check failure on line 185 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / build-test

Delete `⏎⏎····`


}

onExit() {}
Expand All @@ -124,6 +194,7 @@ class AddNoteState implements IState<State, Input, Context> {
readonly id = "addNote";

private readonly cursorPosAtStart: PositionOnSequencer;
private readonly targetTrackId: TrackId;

private currentCursorPos: PositionOnSequencer;
private innerContext:
Expand All @@ -134,8 +205,10 @@ class AddNoteState implements IState<State, Input, Context> {
}
| undefined;

constructor(cursorPosAtStart: PositionOnSequencer) {
constructor(cursorPosAtStart: PositionOnSequencer, targetTrackId: TrackId) {
this.cursorPosAtStart = cursorPosAtStart;
this.targetTrackId = targetTrackId;

this.currentCursorPos = cursorPosAtStart;
}

Expand Down Expand Up @@ -166,8 +239,6 @@ class AddNoteState implements IState<State, Input, Context> {
}

onEnter(context: Context) {
context.storeActions.deselectAllNotes();

const guideLineTicks = getGuideLineTicks(this.cursorPosAtStart, context);
const noteToAdd = {
id: NoteId(crypto.randomUUID()),
Expand Down Expand Up @@ -231,14 +302,171 @@ class AddNoteState implements IState<State, Input, Context> {
const previewNoteIds = previewNotes.map((value) => value.id);

cancelAnimationFrame(this.innerContext.previewRequestId);
context.storeActions.commandAddNotes(context.previewNotes.value);

context.storeActions.commandAddNotes(context.previewNotes.value, this.targetTrackId);

Check failure on line 306 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `context.previewNotes.value,·this.targetTrackId` with `⏎······context.previewNotes.value,⏎······this.targetTrackId,⏎····`

Check failure on line 306 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / build-test

Replace `context.previewNotes.value,·this.targetTrackId` with `⏎······context.previewNotes.value,⏎······this.targetTrackId,⏎····`
context.storeActions.selectNotes(previewNoteIds);

if (previewNotes.length === 1) {
context.storeActions.playPreviewSound(
previewNotes[0].noteNumber,
PREVIEW_SOUND_DURATION,
);
}
context.nowPreviewing.value = false;
}
}

class MoveNoteState implements IState<State, Input, Context> {
readonly id = "moveNote";

private readonly cursorPosAtStart: PositionOnSequencer;
private readonly targetTrackId: TrackId;
private readonly clonedTargetNotes: Map<NoteId, Note>;
private readonly mouseDownNoteId: NoteId;

private currentCursorPos: PositionOnSequencer;

Check failure on line 328 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / lint

Delete `··`

Check failure on line 328 in src/sing/stateMachine/sequencerStateMachine.ts

View workflow job for this annotation

GitHub Actions / build-test

Delete `··`
private innerContext:
| {
previewRequestId: number;
executePreviewProcess: boolean;
edited: boolean;
guideLineTicksAtStart: number;
}
| undefined;

constructor(cursorPosAtStart: PositionOnSequencer, targetTrackId: TrackId, targetNotes: Note[], mouseDownNoteId: NoteId) {
if (!(targetNotes.some(value => value.id === mouseDownNoteId))) {
throw new Error("mouseDownNote is not included in targetNotes.");
}
this.cursorPosAtStart = cursorPosAtStart;
this.targetTrackId = targetTrackId;
this.clonedTargetNotes = new Map();
for (const targetNote of targetNotes) {
this.clonedTargetNotes.set(targetNote.id, { ...targetNote });
}
this.mouseDownNoteId = mouseDownNoteId;

this.currentCursorPos = cursorPosAtStart;
}

private previewMove(context: Context) {
if (this.innerContext == undefined) {
throw new Error("innerContext is undefined.");
}
const snapTicks = context.snapTicks.value;
const previewNotes = context.previewNotes.value;
const clonedTargetNotes = this.clonedTargetNotes;
const mouseDownNote = getOrThrow(clonedTargetNotes, this.mouseDownNoteId);
const dragTicks = this.currentCursorPos.ticks - this.cursorPosAtStart.ticks;
const notePos = mouseDownNote.position;
const newNotePos = Math.round((notePos + dragTicks) / snapTicks) * snapTicks;
const movingTicks = newNotePos - notePos;
const movingSemitones = this.currentCursorPos.noteNumber - this.cursorPosAtStart.noteNumber;

const editedNotes = new Map<NoteId, Note>();
for (const note of previewNotes) {
const clonedTargetNote = clonedTargetNotes.get(note.id);
if (!clonedTargetNote) {
throw new Error("clonedTargetNote is undefined.");
}
const position = clonedTargetNote.position + movingTicks;
const noteNumber = clonedTargetNote.noteNumber + movingSemitones;
if (note.position !== position || note.noteNumber !== noteNumber) {
editedNotes.set(note.id, { ...note, noteNumber, position });
}
}

for (const note of editedNotes.values()) {
if (note.noteNumber < 0 || note.noteNumber >= keyInfos.length) {
// MIDIキー範囲外へはドラッグしない
return;
}
if (note.position < 0) {
// 左端より前はドラッグしない
return;
}
}

if (editedNotes.size !== 0) {
context.previewNotes.value = previewNotes.map((value) => {
return editedNotes.get(value.id) ?? value;
});
this.innerContext.edited = true;
}

context.guideLineTicks.value = this.innerContext.guideLineTicksAtStart + movingTicks;
}

onEnter(context: Context) {
const guideLineTicks = getGuideLineTicks(this.cursorPosAtStart, context);

context.previewNotes.value = [...(this.clonedTargetNotes.values())];
context.nowPreviewing.value = true;

const preview = () => {
if (this.innerContext == undefined) {
throw new Error("innerContext is undefined.");
}
if (this.innerContext.executePreviewProcess) {
this.previewMove(context);
this.innerContext.executePreviewProcess = false;
}
this.innerContext.previewRequestId = requestAnimationFrame(preview);
};
const previewRequestId = requestAnimationFrame(preview);

this.innerContext = {
executePreviewProcess: false,
previewRequestId,
edited: false,
guideLineTicksAtStart: guideLineTicks
};
}

process({
input,
setNextState,
}: {
input: Input;
context: Context;
setNextState: (nextState: State) => void;
}) {
if (this.innerContext == undefined) {
throw new Error("innerContext is undefined.");
}
const mouseButton = getButton(input.mouseEvent);
if (input.targetArea === "SequencerBody") {
if (input.mouseEvent.type === "mousemove") {
this.currentCursorPos = input.cursorPos;
this.innerContext.executePreviewProcess = true;
} else if (input.mouseEvent.type === "mouseup") {
if (mouseButton === "LEFT_BUTTON") {
setNextState(new IdleState());
}
}
}
}

onExit(context: Context) {
if (this.innerContext == undefined) {
throw new Error("innerContext is undefined.");
}
const previewNotes = context.previewNotes.value;
const previewNoteIds = previewNotes.map((value) => value.id);

cancelAnimationFrame(this.innerContext.previewRequestId);

context.storeActions.commandUpdateNotes(previewNotes, this.targetTrackId);
context.storeActions.selectNotes(previewNoteIds);

if (previewNotes.length === 1) {
context.storeActions.playPreviewSound(
previewNotes[0].noteNumber,
PREVIEW_SOUND_DURATION,
);
}
context.previewNotes.value = [];
context.nowPreviewing.value = false;
}
}
Expand Down

0 comments on commit d37de3c

Please sign in to comment.