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

[ソング] Undo/Redoの実装 #1836

Merged
merged 36 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
822cb16
[wip] add song undo redo function
y-chan Feb 9, 2024
8525f05
improve last command time
y-chan Feb 9, 2024
80aec89
clear song commands too
y-chan Feb 9, 2024
f0d170a
add undo redo button to song toolbar
y-chan Feb 9, 2024
ae484b6
Merge branch 'main' into feat/undo-redo-in-song-editor
y-chan Feb 18, 2024
4636ac8
resolve conflict
y-chan Feb 18, 2024
3e29803
fix vuex test
y-chan Feb 18, 2024
13cd984
Merge remote-tracking branch 'upstream/main' into feat/undo-redo-in-s…
y-chan Feb 18, 2024
ecd8bbe
improve command set singer (add setup singer)
y-chan Feb 18, 2024
22056db
refactor set tempo
y-chan Feb 18, 2024
c9ef19a
refactor remove tempo
y-chan Feb 18, 2024
e99de74
refactor set time signature
y-chan Feb 18, 2024
d2b3131
refactor remove time signature
y-chan Feb 18, 2024
1e681f0
refactor add notes
y-chan Feb 18, 2024
f0f9d31
refactor update notes
y-chan Feb 18, 2024
774abcb
refactor remove notes
y-chan Feb 18, 2024
5767402
remove remove selected notes
y-chan Feb 18, 2024
07bdf79
remove comment
y-chan Feb 18, 2024
c86ee16
add COMMAND_SET_VOICE_KEY_SHIFT
y-chan Feb 18, 2024
e8b7665
revert comments
y-chan Feb 19, 2024
f52aee7
remove song
y-chan Feb 19, 2024
4a21141
create overlapping note infos type
y-chan Feb 19, 2024
e878b27
remove overlapping notes detector
y-chan Feb 19, 2024
1124ae8
add return type
y-chan Feb 19, 2024
c81dfbd
remove comment
y-chan Feb 19, 2024
64964a7
remove copy and modify destructively
y-chan Feb 19, 2024
b05cc85
add editor type and use it
y-chan Feb 19, 2024
e5cd7e9
integrate undo redo queues of talk and song
y-chan Feb 19, 2024
0651744
must editor type args
y-chan Feb 19, 2024
0a74ca3
refactor LAST_COMMAND_UNIX_MILLISEC
y-chan Feb 19, 2024
4fa1b45
fix store test
y-chan Feb 19, 2024
e7b8f51
update unit test
y-chan Feb 19, 2024
ad479b8
remove export
y-chan Feb 19, 2024
6daef83
move func
y-chan Feb 19, 2024
b698d04
Merge branch 'main' into feat/undo-redo-in-song-editor
y-chan Feb 19, 2024
ed4c761
デザイン微調整
Hiroshiba Feb 20, 2024
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
5 changes: 3 additions & 2 deletions src/components/Menu/MenuBar/TitleBarEditorSwitcher.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,22 @@
import { computed } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "@/store";
import { EditorType } from "@/type/preload";

const store = useStore();
const router = useRouter();

const uiLocked = computed(() => store.getters.UI_LOCKED);

const nowEditor = computed<"talk" | "song">(() => {
const nowEditor = computed<EditorType>(() => {
const path = router.currentRoute.value.path;
if (path === "/talk") return "talk";
if (path === "/song") return "song";
window.electron.logWarn(`unknown path: ${path}`);
return "talk";
});

const gotoLink = (editor: "talk" | "song") => {
const gotoLink = (editor: EditorType) => {
router.push("/" + editor);
};
</script>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Sing/CharacterMenuButton/MenuButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ const changeStyleId = (speakerUuid: SpeakerId, styleId: StyleId) => {
`No engineId for target character style (speakerUuid == ${speakerUuid}, styleId == ${styleId})`
);

store.dispatch("SET_SINGER", { singer: { engineId, styleId } });
store.dispatch("COMMAND_SET_SINGER", { singer: { engineId, styleId } });
};

const getDefaultStyle = (speakerUuid: string) => {
Expand Down
16 changes: 8 additions & 8 deletions src/components/Sing/ScoreSequencer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -733,12 +733,12 @@ const onMouseUp = (event: MouseEvent) => {
cancelAnimationFrame(previewRequestId);
if (edited) {
if (previewMode === "ADD") {
store.dispatch("ADD_NOTES", { notes: previewNotes.value });
store.dispatch("COMMAND_ADD_NOTES", { notes: previewNotes.value });
store.dispatch("SELECT_NOTES", {
noteIds: previewNotes.value.map((value) => value.id),
});
} else {
store.dispatch("UPDATE_NOTES", { notes: previewNotes.value });
store.dispatch("COMMAND_UPDATE_NOTES", { notes: previewNotes.value });
}
if (previewNotes.value.length === 1) {
store.dispatch("PLAY_PREVIEW_SOUND", {
Expand Down Expand Up @@ -783,7 +783,7 @@ const handleNotesArrowUp = () => {
if (editedNotes.some((note) => note.noteNumber > 127)) {
return;
}
store.dispatch("UPDATE_NOTES", { notes: editedNotes });
store.dispatch("COMMAND_UPDATE_NOTES", { notes: editedNotes });

if (editedNotes.length === 1) {
store.dispatch("PLAY_PREVIEW_SOUND", {
Expand All @@ -802,7 +802,7 @@ const handleNotesArrowDown = () => {
if (editedNotes.some((note) => note.noteNumber < 0)) {
return;
}
store.dispatch("UPDATE_NOTES", { notes: editedNotes });
store.dispatch("COMMAND_UPDATE_NOTES", { notes: editedNotes });

if (editedNotes.length === 1) {
store.dispatch("PLAY_PREVIEW_SOUND", {
Expand All @@ -822,7 +822,7 @@ const handleNotesArrowRight = () => {
// TODO: 例外処理は`UPDATE_NOTES`内に移す?
return;
}
store.dispatch("UPDATE_NOTES", { notes: editedNotes });
store.dispatch("COMMAND_UPDATE_NOTES", { notes: editedNotes });
};

const handleNotesArrowLeft = () => {
Expand All @@ -837,15 +837,15 @@ const handleNotesArrowLeft = () => {
) {
return;
}
store.dispatch("UPDATE_NOTES", { notes: editedNotes });
store.dispatch("COMMAND_UPDATE_NOTES", { notes: editedNotes });
};

const handleNotesBackspaceOrDelete = () => {
if (state.selectedNoteIds.size === 0) {
// TODO: 例外処理は`REMOVE_SELECTED_NOTES`内に移す?
// TODO: 例外処理は`COMMAND_REMOVE_SELECTED_NOTES`内に移す?
return;
}
store.dispatch("REMOVE_SELECTED_NOTES");
store.dispatch("COMMAND_REMOVE_SELECTED_NOTES");
};

const handleKeydown = (event: KeyboardEvent) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Sing/SequencerNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const lyric = computed({
return;
}
const note: Note = { ...props.note, lyric: value };
store.dispatch("UPDATE_NOTES", { notes: [note] });
store.dispatch("COMMAND_UPDATE_NOTES", { notes: [note] });
},
});
const showLyricInput = computed(() => {
Expand All @@ -123,7 +123,7 @@ const contextMenuData = ref<[MenuItemButton]>([
label: "削除",
onClick: async () => {
contextMenu.value?.hide();
store.dispatch("REMOVE_SELECTED_NOTES");
store.dispatch("COMMAND_REMOVE_SELECTED_NOTES");
},
disableWhenUiLocked: true,
},
Expand Down
45 changes: 42 additions & 3 deletions src/components/Sing/ToolBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@
</div>
<!-- settings for edit controls -->
<div class="sing-controls">
<q-btn
flat
dense
round
icon="undo"
class="sing-undo-button"
:disable="!canUndo"
@click="undo"
/>
<q-btn
flat
dense
round
icon="redo"
class="sing-redo-button"
:disable="!canRedo"
@click="redo"
/>
<q-icon name="volume_up" size="xs" class="sing-volume-icon" />
<q-slider v-model.number="volume" class="sing-volume" />
<q-select
Expand Down Expand Up @@ -118,6 +136,17 @@ import CharacterMenuButton from "@/components/Sing/CharacterMenuButton/MenuButto

const store = useStore();

const editor = "song";
const canUndo = computed(() => store.getters.CAN_UNDO(editor));
const canRedo = computed(() => store.getters.CAN_REDO(editor));

const undo = () => {
store.dispatch("UNDO", { editor });
};
const redo = () => {
store.dispatch("REDO", { editor });
};

const tempos = computed(() => store.state.tempos);
const timeSignatures = computed(() => store.state.timeSignatures);
const keyShift = computed(() => store.getters.SELECTED_TRACK.voiceKeyShift);
Expand Down Expand Up @@ -182,7 +211,7 @@ const setKeyShiftInputBuffer = (keyShiftStr: string | number | null) => {

const setTempo = () => {
const bpm = bpmInputBuffer.value;
store.dispatch("SET_TEMPO", {
store.dispatch("COMMAND_SET_TEMPO", {
tempo: {
position: 0,
bpm,
Expand All @@ -193,7 +222,7 @@ const setTempo = () => {
const setTimeSignature = () => {
const beats = beatsInputBuffer.value;
const beatType = beatTypeInputBuffer.value;
store.dispatch("SET_TIME_SIGNATURE", {
store.dispatch("COMMAND_SET_TIME_SIGNATURE", {
timeSignature: {
measureNumber: 1,
beats,
Expand All @@ -204,7 +233,7 @@ const setTimeSignature = () => {

const setKeyShift = () => {
const voiceKeyShift = keyShiftInputBuffer.value;
store.dispatch("SET_VOICE_KEY_SHIFT", { voiceKeyShift });
store.dispatch("COMMAND_SET_VOICE_KEY_SHIFT", { voiceKeyShift });
};

const playheadTicks = ref(0);
Expand Down Expand Up @@ -407,6 +436,16 @@ onUnmounted(() => {
flex: 1;
}

.sing-undo-button,
.sing-redo-button {
&.disabled {
opacity: 0.4 !important;
}
}
.sing-redo-button {
margin-right: 16px;
}

.sing-volume-icon {
margin-right: 8px;
opacity: 0.6;
Expand Down
9 changes: 5 additions & 4 deletions src/components/Talk/ToolBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ type SpacerContent = {
const store = useStore();

const uiLocked = computed(() => store.getters.UI_LOCKED);
const canUndo = computed(() => store.getters.CAN_UNDO);
const canRedo = computed(() => store.getters.CAN_REDO);
const editor = "talk";
const canUndo = computed(() => store.getters.CAN_UNDO(editor));
const canRedo = computed(() => store.getters.CAN_REDO(editor));
const activeAudioKey = computed(() => store.getters.ACTIVE_AUDIO_KEY);
const nowPlayingContinuously = computed(
() => store.state.nowPlayingContinuously
Expand Down Expand Up @@ -98,10 +99,10 @@ const hotkeyMap = new Map<HotkeyActionType, () => HotkeyReturnType>([
setHotkeyFunctions(hotkeyMap);

const undo = () => {
store.dispatch("UNDO");
store.dispatch("UNDO", { editor });
};
const redo = () => {
store.dispatch("REDO");
store.dispatch("REDO", { editor });
};
const playContinuously = async () => {
try {
Expand Down
144 changes: 73 additions & 71 deletions src/sing/storeHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,89 +60,91 @@ type NoteInfo = {
overlappingNoteIds: Set<string>;
};

/**
* 重なっているノートを検出します。
*/
export class OverlappingNotesDetector {
private readonly noteInfos = new Map<string, NoteInfo>();

addNotes(notes: Note[]) {
for (const note of notes) {
this.noteInfos.set(note.id, {
startTicks: note.position,
endTicks: note.position + note.duration,
overlappingNoteIds: new Set<string>(),
});
}
// TODO: 計算量がO(n^2)になっているので、区間木などを使用してO(nlogn)にする
for (const note of notes) {
const overlappingNoteIds = new Set<string>();
for (const [noteId, noteInfo] of this.noteInfos) {
if (noteId === note.id) {
continue;
}
if (noteInfo.startTicks >= note.position + note.duration) {
continue;
}
if (noteInfo.endTicks <= note.position) {
continue;
}
overlappingNoteIds.add(noteId);
export type OverlappingNoteInfos = Map<string, NoteInfo>;

export function addNotesToOverlappingNoteInfos(
overlappingNoteInfos: OverlappingNoteInfos,
notes: Note[]
): void {
for (const note of notes) {
overlappingNoteInfos.set(note.id, {
startTicks: note.position,
endTicks: note.position + note.duration,
overlappingNoteIds: new Set(),
});
}
// TODO: 計算量がO(n^2)になっているので、区間木などを使用してO(nlogn)にする
for (const note of notes) {
const overlappingNoteIds = new Set<string>();
for (const [noteId, noteInfo] of overlappingNoteInfos) {
if (noteId === note.id) {
continue;
}

const noteId1 = note.id;
const noteInfo1 = this.noteInfos.get(noteId1);
if (!noteInfo1) {
throw new Error("noteInfo1 is undefined.");
if (noteInfo.startTicks >= note.position + note.duration) {
continue;
}
for (const noteId2 of overlappingNoteIds) {
const noteInfo2 = this.noteInfos.get(noteId2);
if (!noteInfo2) {
throw new Error("noteInfo2 is undefined.");
}
noteInfo2.overlappingNoteIds.add(noteId1);
noteInfo1.overlappingNoteIds.add(noteId2);
if (noteInfo.endTicks <= note.position) {
continue;
}
overlappingNoteIds.add(noteId);
}
}

removeNotes(notes: Note[]) {
for (const note of notes) {
const noteId1 = note.id;
const noteInfo1 = this.noteInfos.get(noteId1);
if (!noteInfo1) {
throw new Error("noteInfo1 is undefined.");
}
for (const noteId2 of noteInfo1.overlappingNoteIds) {
const noteInfo2 = this.noteInfos.get(noteId2);
if (!noteInfo2) {
throw new Error("noteInfo2 is undefined.");
}
noteInfo2.overlappingNoteIds.delete(noteId1);
noteInfo1.overlappingNoteIds.delete(noteId2);
}
const noteId1 = note.id;
const noteInfo1 = overlappingNoteInfos.get(noteId1);
if (!noteInfo1) {
throw new Error("noteInfo1 is undefined.");
}
for (const note of notes) {
this.noteInfos.delete(note.id);
for (const noteId2 of overlappingNoteIds) {
const noteInfo2 = overlappingNoteInfos.get(noteId2);
if (!noteInfo2) {
throw new Error("noteInfo2 is undefined.");
}
noteInfo2.overlappingNoteIds.add(noteId1);
noteInfo1.overlappingNoteIds.add(noteId2);
}
}
}

updateNotes(notes: Note[]) {
this.removeNotes(notes);
this.addNotes(notes);
}

getOverlappingNoteIds() {
const overlappingNoteIds = new Set<string>();
for (const [noteId, noteInfo] of this.noteInfos) {
if (noteInfo.overlappingNoteIds.size !== 0) {
overlappingNoteIds.add(noteId);
export function removeNotesFromOverlappingNoteInfos(
overlappingNoteInfos: OverlappingNoteInfos,
notes: Note[]
): void {
for (const note of notes) {
const noteId1 = note.id;
const noteInfo1 = overlappingNoteInfos.get(noteId1);
if (!noteInfo1) {
throw new Error("noteInfo1 is undefined.");
}
for (const noteId2 of noteInfo1.overlappingNoteIds) {
const noteInfo2 = overlappingNoteInfos.get(noteId2);
if (!noteInfo2) {
throw new Error("noteInfo2 is undefined.");
}
noteInfo2.overlappingNoteIds.delete(noteId1);
noteInfo1.overlappingNoteIds.delete(noteId2);
}
return overlappingNoteIds;
}
for (const note of notes) {
overlappingNoteInfos.delete(note.id);
}
}

clear() {
this.noteInfos.clear();
export function updateNotesOfOverlappingNoteInfos(
overlappingNoteInfos: OverlappingNoteInfos,
notes: Note[]
): void {
removeNotesFromOverlappingNoteInfos(overlappingNoteInfos, notes);
addNotesToOverlappingNoteInfos(overlappingNoteInfos, notes);
}

export function getOverlappingNoteIds(
currentNoteInfos: OverlappingNoteInfos
): Set<string> {
const overlappingNoteIds = new Set<string>();
for (const [noteId, noteInfo] of currentNoteInfos) {
if (noteInfo.overlappingNoteIds.size !== 0) {
overlappingNoteIds.add(noteId);
}
}
return overlappingNoteIds;
}
3 changes: 2 additions & 1 deletion src/store/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2867,5 +2867,6 @@ export const audioCommandStore = transformCommandStore(
}
),
},
})
}),
"talk"
);
Loading
Loading