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] ResizeNoteLeftStateを追加 #2463

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
197 changes: 169 additions & 28 deletions src/sing/stateMachine/sequencerStateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
*/

import { computed, ComputedRef, ref, Ref } from "vue";
import { clamp } from "../utility";
import { IState, StateMachine } from "@/sing/stateMachine/stateMachineBase";
import {
getButton,
getDoremiFromNoteNumber,
isSelfEventTarget,
keyInfos,
PREVIEW_SOUND_DURATION,
} from "@/sing/viewHelper";
import { Note, SequencerEditTarget } from "@/store/type";
Expand Down Expand Up @@ -72,7 +72,7 @@ type StoreActions = {

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

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

const getGuideLineTicks = (
cursorPos: PositionOnSequencer,
Expand Down Expand Up @@ -166,13 +166,6 @@ class IdleState implements IState<State, Input, Context> {
return;
}
if (mouseButton === "LEFT_BUTTON") {
if (
input.cursorPos.ticks < 0 ||
input.cursorPos.noteNumber < 0 ||
input.cursorPos.noteNumber > 127
) {
return;
}
context.storeActions.deselectAllNotes();
const addNoteState = new AddNoteState(
input.cursorPos,
Expand Down Expand Up @@ -256,9 +249,9 @@ class AddNoteState implements IState<State, Input, Context> {
const guideLineTicks = getGuideLineTicks(this.cursorPosAtStart, context);
const noteToAdd = {
id: NoteId(crypto.randomUUID()),
position: guideLineTicks,
position: Math.max(0, guideLineTicks),
duration: context.snapTicks.value,
noteNumber: this.cursorPosAtStart.noteNumber,
noteNumber: clamp(this.cursorPosAtStart.noteNumber, 0, 127),
lyric: getDoremiFromNoteNumber(this.cursorPosAtStart.noteNumber),
};

Expand Down Expand Up @@ -390,28 +383,19 @@ class MoveNoteState implements IState<State, Input, Context> {

const editedNotes = new Map<NoteId, Note>();
for (const note of previewNotes) {
const targetNoteAtStart = targetNotesAtStart.get(note.id);
if (!targetNoteAtStart) {
throw new Error("targetNoteAtStart is undefined.");
}
const position = targetNoteAtStart.position + movingTicks;
const noteNumber = targetNoteAtStart.noteNumber + movingSemitones;
const targetNoteAtStart = getOrThrow(targetNotesAtStart, note.id);
const position = Math.max(0, targetNoteAtStart.position + movingTicks);
const noteNumber = clamp(
targetNoteAtStart.noteNumber + movingSemitones,
0,
127,
);

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;
Expand Down Expand Up @@ -505,6 +489,163 @@ class MoveNoteState implements IState<State, Input, Context> {
}
}

class ResizeNoteLeftState implements IState<State, Input, Context> {
readonly id = "resizeNoteLeft";

private readonly cursorPosAtStart: PositionOnSequencer;
private readonly targetTrackId: TrackId;
private readonly targetNoteIds: Set<NoteId>;
private readonly mouseDownNoteId: NoteId;

private currentCursorPos: PositionOnSequencer;

private innerContext:
| {
targetNotesAtStart: Map<NoteId, Note>;
previewRequestId: number;
executePreviewProcess: boolean;
edited: boolean;
guideLineTicksAtStart: number;
}
| undefined;

constructor(
cursorPosAtStart: PositionOnSequencer,
targetTrackId: TrackId,
targetNoteIds: Set<NoteId>,
mouseDownNoteId: NoteId,
) {
if (!targetNoteIds.has(mouseDownNoteId)) {
throw new Error("mouseDownNoteId is not included in targetNoteIds.");
}
this.cursorPosAtStart = cursorPosAtStart;
this.targetTrackId = targetTrackId;
this.targetNoteIds = targetNoteIds;
this.mouseDownNoteId = mouseDownNoteId;

this.currentCursorPos = cursorPosAtStart;
}

private previewResizeLeft(context: Context) {
if (this.innerContext == undefined) {
throw new Error("innerContext is undefined.");
}
const snapTicks = context.snapTicks.value;
const previewNotes = context.previewNotes.value;
const targetNotesAtStart = this.innerContext.targetNotesAtStart;
const mouseDownNote = getOrThrow(targetNotesAtStart, 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 editedNotes = new Map<NoteId, Note>();
for (const note of previewNotes) {
const targetNoteAtStart = getOrThrow(targetNotesAtStart, note.id);
const notePos = targetNoteAtStart.position;
const noteEndPos =
targetNoteAtStart.position + targetNoteAtStart.duration;
const position = clamp(notePos + movingTicks, 0, noteEndPos - snapTicks);
const duration = noteEndPos - position;

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

context.guideLineTicks.value = newNotePos;
}

onEnter(context: Context) {
const guideLineTicks = getGuideLineTicks(this.cursorPosAtStart, context);
const targetNotesArray = context.notesInSelectedTrack.value.filter(
(value) => this.targetNoteIds.has(value.id),
);
const targetNotesMap = new Map<NoteId, Note>();
for (const targetNote of targetNotesArray) {
targetNotesMap.set(targetNote.id, targetNote);
}

context.previewNotes.value = [...targetNotesArray];
context.nowPreviewing.value = true;

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

this.innerContext = {
targetNotesAtStart: targetNotesMap,
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" &&
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;
}
}

export const useSequencerStateMachine = (
computedRefs: ComputedRefs,
storeActions: StoreActions,
Expand Down
9 changes: 9 additions & 0 deletions src/sing/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ export function round(value: number, digits: number) {
return Math.round(value * powerOf10) / powerOf10;
}

export function clamp(value: number, min: number, max: number) {
if (min > max) {
throw new Error(
`Invalid range: min (${min}) cannot be greater than max (${max}).`,
);
}
return Math.min(max, Math.max(min, value));
}

export function getLast<T>(array: T[]) {
if (array.length === 0) {
throw new Error("array.length is 0.");
Expand Down
Loading