Skip to content

Commit

Permalink
Refactor: SequencerRulerをContainer/Presentationに分離 (VOICEVOX#2312)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Hiroshiba <[email protected]>
Co-authored-by: Hiroshiba Kazuyuki <[email protected]>
  • Loading branch information
4 people authored Oct 23, 2024
1 parent 428413c commit d7087ef
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 49 deletions.
2 changes: 1 addition & 1 deletion src/components/Sing/ScoreSequencer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ import {
PreviewMode,
} from "@/sing/viewHelper";
import SequencerGrid from "@/components/Sing/SequencerGrid/Container.vue";
import SequencerRuler from "@/components/Sing/SequencerRuler.vue";
import SequencerRuler from "@/components/Sing/SequencerRuler/Container.vue";
import SequencerKeys from "@/components/Sing/SequencerKeys.vue";
import SequencerNote from "@/components/Sing/SequencerNote.vue";
import SequencerShadowNote from "@/components/Sing/SequencerShadowNote.vue";
Expand Down
63 changes: 63 additions & 0 deletions src/components/Sing/SequencerRuler/Container.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<template>
<Presentation
:offset
:tpqn
:timeSignatures
:sequencerZoomX
:numMeasures
:playheadTicks
@update:playheadTicks="updatePlayheadTicks"
@deselectAllNotes="deselectAllNotes"
/>
</template>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue";
import Presentation from "./Presentation.vue";
import { useStore } from "@/store";
defineOptions({
name: "SequencerRuler",
});
withDefaults(
defineProps<{
offset: number;
numMeasures: number;
}>(),
{
offset: 0,
numMeasures: 32,
},
);
const store = useStore();
const tpqn = computed(() => store.state.tpqn);
const timeSignatures = computed(() => store.state.timeSignatures);
const sequencerZoomX = computed(() => store.state.sequencerZoomX);
const playheadTicks = ref(0);
const updatePlayheadTicks = (ticks: number) => {
void store.dispatch("SET_PLAYHEAD_POSITION", { position: ticks });
};
const playheadPositionChangeListener = (position: number) => {
playheadTicks.value = position;
};
onMounted(() => {
void store.dispatch("ADD_PLAYHEAD_POSITION_CHANGE_LISTENER", {
listener: playheadPositionChangeListener,
});
});
onUnmounted(() => {
void store.dispatch("REMOVE_PLAYHEAD_POSITION_CHANGE_LISTENER", {
listener: playheadPositionChangeListener,
});
});
const deselectAllNotes = () => {
void store.dispatch("DESELECT_ALL_NOTES");
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -62,60 +62,57 @@

<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from "vue";
import { useStore } from "@/store";
import { getMeasureDuration, getTimeSignaturePositions } from "@/sing/domain";
import { baseXToTick, tickToBaseX } from "@/sing/viewHelper";
import { TimeSignature } from "@/store/type";
const props = defineProps<{
offset: number;
numMeasures: number;
tpqn: number;
timeSignatures: TimeSignature[];
sequencerZoomX: number;
}>();
const playheadTicks = defineModel<number>("playheadTicks", { required: true });
const emit = defineEmits<{
deselectAllNotes: [];
}>();
const props = withDefaults(
defineProps<{
offset: number;
numMeasures: number;
}>(),
{
offset: 0,
numMeasures: 32,
},
);
const store = useStore();
const state = store.state;
const height = ref(40);
const playheadTicks = ref(0);
const tpqn = computed(() => state.tpqn);
const timeSignatures = computed(() => state.timeSignatures);
const zoomX = computed(() => state.sequencerZoomX);
const beatsPerMeasure = computed(() => {
return timeSignatures.value[0].beats;
return props.timeSignatures[0].beats;
});
const beatWidth = computed(() => {
const beatType = timeSignatures.value[0].beatType;
const wholeNoteDuration = tpqn.value * 4;
const beatType = props.timeSignatures[0].beatType;
const wholeNoteDuration = props.tpqn * 4;
const beatTicks = wholeNoteDuration / beatType;
return tickToBaseX(beatTicks, tpqn.value) * zoomX.value;
return tickToBaseX(beatTicks, props.tpqn) * props.sequencerZoomX;
});
const tsPositions = computed(() => {
return getTimeSignaturePositions(timeSignatures.value, tpqn.value);
return getTimeSignaturePositions(props.timeSignatures, props.tpqn);
});
const endTicks = computed(() => {
const lastTs = timeSignatures.value[timeSignatures.value.length - 1];
const lastTs = props.timeSignatures[props.timeSignatures.length - 1];
const lastTsPosition = tsPositions.value[tsPositions.value.length - 1];
return (
lastTsPosition +
getMeasureDuration(lastTs.beats, lastTs.beatType, tpqn.value) *
getMeasureDuration(lastTs.beats, lastTs.beatType, props.tpqn) *
(props.numMeasures - lastTs.measureNumber + 1)
);
});
const width = computed(() => {
return tickToBaseX(endTicks.value, tpqn.value) * zoomX.value;
return tickToBaseX(endTicks.value, props.tpqn) * props.sequencerZoomX;
});
const measureInfos = computed(() => {
return timeSignatures.value.flatMap((timeSignature, i) => {
return props.timeSignatures.flatMap((timeSignature, i) => {
const measureDuration = getMeasureDuration(
timeSignature.beats,
timeSignature.beatType,
tpqn.value,
props.tpqn,
);
const nextTsPosition =
i !== timeSignatures.value.length - 1
i !== props.timeSignatures.length - 1
? tsPositions.value[i + 1]
: endTicks.value;
const start = tsPositions.value[i];
Expand All @@ -124,38 +121,34 @@ const measureInfos = computed(() => {
return Array.from({ length: numMeasures }, (_, index) => {
const measureNumber = timeSignature.measureNumber + index;
const measurePosition = start + index * measureDuration;
const baseX = tickToBaseX(measurePosition, tpqn.value);
const baseX = tickToBaseX(measurePosition, props.tpqn);
return {
number: measureNumber,
x: Math.round(baseX * zoomX.value),
x: Math.round(baseX * props.sequencerZoomX),
};
});
});
});
const playheadX = computed(() => {
const baseX = tickToBaseX(playheadTicks.value, tpqn.value);
return Math.floor(baseX * zoomX.value);
const baseX = tickToBaseX(playheadTicks.value, props.tpqn);
return Math.floor(baseX * props.sequencerZoomX);
});
const onClick = (event: MouseEvent) => {
void store.dispatch("DESELECT_ALL_NOTES");
emit("deselectAllNotes");
const sequencerRulerElement = sequencerRuler.value;
if (!sequencerRulerElement) {
throw new Error("sequencerRulerElement is null.");
}
const baseX = (props.offset + event.offsetX) / zoomX.value;
const ticks = baseXToTick(baseX, tpqn.value);
void store.dispatch("SET_PLAYHEAD_POSITION", { position: ticks });
const baseX = (props.offset + event.offsetX) / props.sequencerZoomX;
const ticks = baseXToTick(baseX, props.tpqn);
playheadTicks.value = ticks;
};
const sequencerRuler = ref<HTMLElement | null>(null);
let resizeObserver: ResizeObserver | undefined;
const playheadPositionChangeListener = (position: number) => {
playheadTicks.value = position;
};
onMounted(() => {
const sequencerRulerElement = sequencerRuler.value;
if (!sequencerRulerElement) {
Expand All @@ -173,18 +166,10 @@ onMounted(() => {
}
});
resizeObserver.observe(sequencerRulerElement);
void store.dispatch("ADD_PLAYHEAD_POSITION_CHANGE_LISTENER", {
listener: playheadPositionChangeListener,
});
});
onUnmounted(() => {
resizeObserver?.disconnect();
void store.dispatch("REMOVE_PLAYHEAD_POSITION_CHANGE_LISTENER", {
listener: playheadPositionChangeListener,
});
});
</script>

Expand Down
73 changes: 73 additions & 0 deletions src/components/Sing/SequencerRuler/index.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { Meta, StoryObj } from "@storybook/vue3";
import { fn, expect, Mock } from "@storybook/test";
import { ref } from "vue";

import Presentation from "./Presentation.vue";
import { UnreachableError } from "@/type/utility";

const meta: Meta<typeof Presentation> = {
component: Presentation,
args: {
timeSignatures: [
{
beats: 4,
beatType: 4,
measureNumber: 1,
},
],
sequencerZoomX: 0.25,
tpqn: 480,
offset: 0,
numMeasures: 32,
"onUpdate:playheadTicks": fn<(value: number) => void>(),
onDeselectAllNotes: fn(),
},
render: (args) => ({
components: { Presentation },
setup() {
const playheadTicks = ref(0);
return { args, playheadTicks };
},
template: `<Presentation v-bind="args" v-model:playheadTicks="playheadTicks" />`,
}),
};

export default meta;
type Story = StoryObj<typeof Presentation>;

export const Default: Story = {};

export const MovePlayhead: Story = {
name: "再生位置を移動",

play: async ({ canvasElement, args }) => {
const ruler =
canvasElement.querySelector<HTMLDivElement>(".sequencer-ruler");

if (!ruler) {
throw new UnreachableError("ruler is not found");
}

// userEvent.pointerは座標指定が上手くいかないので、MouseEventを使って手動でクリックをエミュレートする
const rect = ruler.getBoundingClientRect();
const width = rect.width;
const event = new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: rect.left + width / 2,
clientY: rect.top + rect.height,
});

ruler.dispatchEvent(event);

await expect(args["onUpdate:playheadTicks"]).toHaveBeenCalled();

const onUpdateCallback = args["onUpdate:playheadTicks"] as Mock<
(value: number) => void
>;
const newTick = onUpdateCallback.mock.calls[0][0];

await expect(newTick).toBeGreaterThan(0);
await expect(args["onDeselectAllNotes"]).toHaveBeenCalled();
},
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d7087ef

Please sign in to comment.