Skip to content

Commit

Permalink
[project-sequencer-statemachine] ResizeNoteLeftStateを追加 (#2463)
Browse files Browse the repository at this point in the history
Co-authored-by: Hiroshiba <[email protected]>
  • Loading branch information
sigprogramming and Hiroshiba authored Jan 3, 2025
1 parent 51b39f2 commit 07783fb
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 28 deletions.
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

0 comments on commit 07783fb

Please sign in to comment.