Skip to content

Commit add6994

Browse files
committed
fix(piano-roll): allow pasting overlapping copied notes
1 parent 4868b65 commit add6994

5 files changed

Lines changed: 100 additions & 23 deletions

File tree

src/components/piano-roll/pianoRollClipboard.js

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ export const buildClipboardPastePayload = function ({
4949
minFreeLength,
5050
pitchMin,
5151
pitchMax,
52-
channelNotes,
5352
defaultVelocity,
5453
clampFn,
5554
makeIdFn,
@@ -74,12 +73,6 @@ export const buildClipboardPastePayload = function ({
7473
? sharedPianoClipboard.pasteCountInSource
7574
: 0;
7675

77-
const occupied = new Set(
78-
(channelNotes || []).map(function (note) {
79-
return Math.round((note.start || 0) * 1000) + ":" + note.pitch;
80-
}),
81-
);
82-
8376
const notesToAdd = [];
8477
const nextSelection = [];
8578

@@ -88,13 +81,6 @@ export const buildClipboardPastePayload = function ({
8881
const maxLen = Math.max(minFreeLength, patternLength - start);
8982
const length = clampFn(entry.length, minFreeLength, maxLen);
9083
const pitch = clampFn(entry.pitch, pitchMin, pitchMax);
91-
const key = Math.round(start * 1000) + ":" + pitch;
92-
93-
if (occupied.has(key)) {
94-
return;
95-
}
96-
97-
occupied.add(key);
9884
const newId = makeIdFn("paste");
9985
notesToAdd.push({
10086
id: newId,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, it, expect } from "vitest";
2+
import {
3+
buildClipboardPastePayload,
4+
copySelectedNotesToClipboard,
5+
} from "./pianoRollClipboard";
6+
7+
function clamp(value, min, max) {
8+
return Math.max(min, Math.min(max, value));
9+
}
10+
11+
function makeId(prefix) {
12+
makeId.count = (makeId.count || 0) + 1;
13+
return prefix + "-" + makeId.count;
14+
}
15+
16+
describe("pianoRollClipboard", () => {
17+
it("keeps overlapping copied notes in paste payload", () => {
18+
copySelectedNotesToClipboard({
19+
activePatternId: "pat-1",
20+
activeChannelId: "ch-1",
21+
defaultVelocity: 95,
22+
clampFn: clamp,
23+
selectedNotes: [
24+
{ start: 0, pitch: 60, length: 4, velocity: 100 },
25+
{ start: 0, pitch: 60, length: 1, velocity: 80 },
26+
],
27+
});
28+
29+
const { notesToAdd, nextSelection } = buildClipboardPastePayload({
30+
activePatternId: "pat-1",
31+
activeChannelId: "ch-1",
32+
patternLength: 16,
33+
minFreeLength: 0.0625,
34+
pitchMin: 0,
35+
pitchMax: 127,
36+
defaultVelocity: 95,
37+
clampFn: clamp,
38+
makeIdFn: makeId,
39+
});
40+
41+
expect(notesToAdd).toHaveLength(2);
42+
expect(notesToAdd[0]).toMatchObject({ start: 1, pitch: 60, length: 4 });
43+
expect(notesToAdd[1]).toMatchObject({ start: 1, pitch: 60, length: 1 });
44+
expect(nextSelection).toHaveLength(2);
45+
});
46+
});

src/components/piano-roll/usePianoRollClipboardActions.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,13 @@ export const usePianoRollClipboardActions = function ({
9595
return;
9696
}
9797

98-
const channelNotes = activePattern.pianoPreview?.[activeChannel.id] || [];
9998
const { notesToAdd, nextSelection } = buildClipboardPastePayload({
10099
activePatternId,
101100
activeChannelId: activeChannel.id,
102101
patternLength,
103102
minFreeLength,
104103
pitchMin,
105104
pitchMax,
106-
channelNotes,
107105
defaultVelocity,
108106
clampFn,
109107
makeIdFn,
@@ -115,6 +113,7 @@ export const usePianoRollClipboardActions = function ({
115113
patternId: activePatternId,
116114
channelId: activeChannel.id,
117115
notes: notesToAdd,
116+
allowOverlaps: true,
118117
}),
119118
);
120119
}

src/store/reducers/project.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,11 +1319,14 @@ export const projectReducers = {
13191319

13201320
const patternLength = Math.max(1, pattern.lengthSteps || 16);
13211321
const notes = pattern.pianoPreview[channelId];
1322-
const occupied = new Set(
1323-
notes.map(function (note) {
1324-
return Math.round((note.start || 0) * 1000) + ":" + note.pitch;
1325-
}),
1326-
);
1322+
const allowOverlaps = Boolean(action.payload.allowOverlaps);
1323+
const occupied = allowOverlaps
1324+
? null
1325+
: new Set(
1326+
notes.map(function (note) {
1327+
return Math.round((note.start || 0) * 1000) + ":" + note.pitch;
1328+
}),
1329+
);
13271330

13281331
incomingNotes.forEach(function (inputNote) {
13291332
const start = Math.max(
@@ -1343,11 +1346,11 @@ export const projectReducers = {
13431346
),
13441347
);
13451348
const key = Math.round(start * 1000) + ":" + pitch;
1346-
if (occupied.has(key)) {
1349+
if (occupied?.has(key)) {
13471350
return;
13481351
}
13491352

1350-
occupied.add(key);
1353+
occupied?.add(key);
13511354
notes.push({
13521355
id:
13531356
inputNote?.id ||

src/store/reducers/project.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,47 @@ describe("projectReducers", () => {
147147
expect(state.project.activePatternId).toBe("pat-1");
148148
});
149149
});
150+
151+
describe("addPianoNotesBatch", () => {
152+
it("skips duplicate start/pitch by default", () => {
153+
const state = createMinimalState();
154+
state.project.patterns[0].pianoPreview["ch-1"] = [
155+
{ id: "existing", start: 1, length: 4, pitch: 60, velocity: 100 },
156+
];
157+
158+
projectReducers.addPianoNotesBatch(state, {
159+
payload: {
160+
patternId: "pat-1",
161+
channelId: "ch-1",
162+
notes: [{ id: "new", start: 1, length: 1, pitch: 60, velocity: 80 }],
163+
},
164+
});
165+
166+
expect(state.project.patterns[0].pianoPreview["ch-1"]).toHaveLength(1);
167+
});
168+
169+
it("allows overlapping notes when requested by paste", () => {
170+
const state = createMinimalState();
171+
state.project.patterns[0].pianoPreview["ch-1"] = [
172+
{ id: "existing", start: 1, length: 4, pitch: 60, velocity: 100 },
173+
];
174+
175+
projectReducers.addPianoNotesBatch(state, {
176+
payload: {
177+
patternId: "pat-1",
178+
channelId: "ch-1",
179+
allowOverlaps: true,
180+
notes: [{ id: "new", start: 1, length: 1, pitch: 60, velocity: 80 }],
181+
},
182+
});
183+
184+
expect(state.project.patterns[0].pianoPreview["ch-1"]).toHaveLength(2);
185+
expect(state.project.patterns[0].pianoPreview["ch-1"][1]).toMatchObject({
186+
id: "new",
187+
start: 1,
188+
length: 1,
189+
pitch: 60,
190+
});
191+
});
192+
});
150193
});

0 commit comments

Comments
 (0)