@sudobility/music_types 0.11.20 → 0.11.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CLAUDE.md +9 -0
  2. package/dist/domain/commands/edit-commands.d.ts +66 -0
  3. package/dist/domain/commands/edit-commands.d.ts.map +1 -0
  4. package/dist/domain/commands/edit-commands.js +185 -0
  5. package/dist/domain/commands/edit-commands.js.map +1 -0
  6. package/dist/domain/commands/note-commands.d.ts +73 -0
  7. package/dist/domain/commands/note-commands.d.ts.map +1 -0
  8. package/dist/domain/commands/note-commands.js +292 -0
  9. package/dist/domain/commands/note-commands.js.map +1 -0
  10. package/dist/domain/commands/reflow.d.ts +69 -0
  11. package/dist/domain/commands/reflow.d.ts.map +1 -0
  12. package/dist/domain/commands/reflow.js +395 -0
  13. package/dist/domain/commands/reflow.js.map +1 -0
  14. package/dist/domain/commands/region-commands.d.ts +21 -0
  15. package/dist/domain/commands/region-commands.d.ts.map +1 -0
  16. package/dist/domain/commands/region-commands.js +47 -0
  17. package/dist/domain/commands/region-commands.js.map +1 -0
  18. package/dist/domain/commands/relocate-commands.d.ts +18 -0
  19. package/dist/domain/commands/relocate-commands.d.ts.map +1 -0
  20. package/dist/domain/commands/relocate-commands.js +108 -0
  21. package/dist/domain/commands/relocate-commands.js.map +1 -0
  22. package/dist/domain/commands/ripple-commands.d.ts +44 -0
  23. package/dist/domain/commands/ripple-commands.d.ts.map +1 -0
  24. package/dist/domain/commands/ripple-commands.js +104 -0
  25. package/dist/domain/commands/ripple-commands.js.map +1 -0
  26. package/dist/domain/commands/snapshot.d.ts +38 -0
  27. package/dist/domain/commands/snapshot.d.ts.map +1 -0
  28. package/dist/domain/commands/snapshot.js +44 -0
  29. package/dist/domain/commands/snapshot.js.map +1 -0
  30. package/dist/domain/commands/structure-commands.d.ts +38 -0
  31. package/dist/domain/commands/structure-commands.d.ts.map +1 -0
  32. package/dist/domain/commands/structure-commands.js +220 -0
  33. package/dist/domain/commands/structure-commands.js.map +1 -0
  34. package/dist/domain/commands/track-commands.d.ts +15 -0
  35. package/dist/domain/commands/track-commands.d.ts.map +1 -0
  36. package/dist/domain/commands/track-commands.js +56 -0
  37. package/dist/domain/commands/track-commands.js.map +1 -0
  38. package/dist/domain/commands/types.d.ts +31 -0
  39. package/dist/domain/commands/types.d.ts.map +1 -0
  40. package/dist/domain/commands/types.js +2 -0
  41. package/dist/domain/commands/types.js.map +1 -0
  42. package/dist/domain/instruments/gm-kit.d.ts +45 -0
  43. package/dist/domain/instruments/gm-kit.d.ts.map +1 -0
  44. package/dist/domain/instruments/gm-kit.js +59 -0
  45. package/dist/domain/instruments/gm-kit.js.map +1 -0
  46. package/dist/domain/instruments/gm-percussion.d.ts +42 -0
  47. package/dist/domain/instruments/gm-percussion.d.ts.map +1 -0
  48. package/dist/domain/instruments/gm-percussion.js +85 -0
  49. package/dist/domain/instruments/gm-percussion.js.map +1 -0
  50. package/dist/domain/instruments/gm-polyphony.d.ts +13 -0
  51. package/dist/domain/instruments/gm-polyphony.d.ts.map +1 -0
  52. package/dist/domain/instruments/gm-polyphony.js +76 -0
  53. package/dist/domain/instruments/gm-polyphony.js.map +1 -0
  54. package/dist/domain/instruments/gm-range.d.ts +15 -0
  55. package/dist/domain/instruments/gm-range.d.ts.map +1 -0
  56. package/dist/domain/instruments/gm-range.js +97 -0
  57. package/dist/domain/instruments/gm-range.js.map +1 -0
  58. package/dist/domain/instruments/gm-transposition.d.ts +11 -0
  59. package/dist/domain/instruments/gm-transposition.d.ts.map +1 -0
  60. package/dist/domain/instruments/gm-transposition.js +69 -0
  61. package/dist/domain/instruments/gm-transposition.js.map +1 -0
  62. package/dist/domain/instruments/gm.d.ts +28 -0
  63. package/dist/domain/instruments/gm.d.ts.map +1 -0
  64. package/dist/domain/instruments/gm.js +217 -0
  65. package/dist/domain/instruments/gm.js.map +1 -0
  66. package/dist/domain/pitch/pitch.d.ts +15 -0
  67. package/dist/domain/pitch/pitch.d.ts.map +1 -0
  68. package/dist/domain/pitch/pitch.js +81 -0
  69. package/dist/domain/pitch/pitch.js.map +1 -0
  70. package/dist/domain/pitch/transpose.d.ts +37 -0
  71. package/dist/domain/pitch/transpose.d.ts.map +1 -0
  72. package/dist/domain/pitch/transpose.js +65 -0
  73. package/dist/domain/pitch/transpose.js.map +1 -0
  74. package/dist/domain/quantization/options.d.ts +55 -0
  75. package/dist/domain/quantization/options.d.ts.map +1 -0
  76. package/dist/domain/quantization/options.js +2 -0
  77. package/dist/domain/quantization/options.js.map +1 -0
  78. package/dist/domain/quantization/quantize.d.ts +13 -0
  79. package/dist/domain/quantization/quantize.d.ts.map +1 -0
  80. package/dist/domain/quantization/quantize.js +199 -0
  81. package/dist/domain/quantization/quantize.js.map +1 -0
  82. package/dist/domain/score/factory.d.ts +39 -0
  83. package/dist/domain/score/factory.d.ts.map +1 -0
  84. package/dist/domain/score/factory.js +148 -0
  85. package/dist/domain/score/factory.js.map +1 -0
  86. package/dist/domain/score/fragment.d.ts +28 -0
  87. package/dist/domain/score/fragment.d.ts.map +1 -0
  88. package/dist/domain/score/fragment.js +68 -0
  89. package/dist/domain/score/fragment.js.map +1 -0
  90. package/dist/domain/score/ids.d.ts +8 -0
  91. package/dist/domain/score/ids.d.ts.map +1 -0
  92. package/dist/domain/score/ids.js +38 -0
  93. package/dist/domain/score/ids.js.map +1 -0
  94. package/dist/domain/score/queries.d.ts +34 -0
  95. package/dist/domain/score/queries.d.ts.map +1 -0
  96. package/dist/domain/score/queries.js +132 -0
  97. package/dist/domain/score/queries.js.map +1 -0
  98. package/dist/domain/score/ties.d.ts +73 -0
  99. package/dist/domain/score/ties.d.ts.map +1 -0
  100. package/dist/domain/score/ties.js +200 -0
  101. package/dist/domain/score/ties.js.map +1 -0
  102. package/dist/domain/selection/selection.d.ts +63 -0
  103. package/dist/domain/selection/selection.d.ts.map +1 -0
  104. package/dist/domain/selection/selection.js +129 -0
  105. package/dist/domain/selection/selection.js.map +1 -0
  106. package/dist/domain/selection/types.d.ts +10 -0
  107. package/dist/domain/selection/types.d.ts.map +1 -0
  108. package/dist/domain/selection/types.js +5 -0
  109. package/dist/domain/selection/types.js.map +1 -0
  110. package/dist/domain/time/durations.d.ts +21 -0
  111. package/dist/domain/time/durations.d.ts.map +1 -0
  112. package/dist/domain/time/durations.js +53 -0
  113. package/dist/domain/time/durations.js.map +1 -0
  114. package/dist/domain/time/fraction.d.ts +19 -0
  115. package/dist/domain/time/fraction.d.ts.map +1 -0
  116. package/dist/domain/time/fraction.js +60 -0
  117. package/dist/domain/time/fraction.js.map +1 -0
  118. package/dist/domain/time/tempo-map.d.ts +24 -0
  119. package/dist/domain/time/tempo-map.d.ts.map +1 -0
  120. package/dist/domain/time/tempo-map.js +85 -0
  121. package/dist/domain/time/tempo-map.js.map +1 -0
  122. package/dist/domain/time/ticks.d.ts +34 -0
  123. package/dist/domain/time/ticks.d.ts.map +1 -0
  124. package/dist/domain/time/ticks.js +93 -0
  125. package/dist/domain/time/ticks.js.map +1 -0
  126. package/dist/domain/validation/issues.d.ts +44 -0
  127. package/dist/domain/validation/issues.d.ts.map +1 -0
  128. package/dist/domain/validation/issues.js +34 -0
  129. package/dist/domain/validation/issues.js.map +1 -0
  130. package/dist/domain/validation/validator.d.ts +11 -0
  131. package/dist/domain/validation/validator.d.ts.map +1 -0
  132. package/dist/domain/validation/validator.js +375 -0
  133. package/dist/domain/validation/validator.js.map +1 -0
  134. package/dist/domain/voicing/allocate.d.ts +19 -0
  135. package/dist/domain/voicing/allocate.d.ts.map +1 -0
  136. package/dist/domain/voicing/allocate.js +100 -0
  137. package/dist/domain/voicing/allocate.js.map +1 -0
  138. package/dist/index.d.ts +46 -4
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/index.js +50 -2
  141. package/dist/index.js.map +1 -1
  142. package/dist/services/regeneration/controller.d.ts +60 -0
  143. package/dist/services/regeneration/controller.d.ts.map +1 -0
  144. package/dist/services/regeneration/controller.js +202 -0
  145. package/dist/services/regeneration/controller.js.map +1 -0
  146. package/dist/test/fixtures.d.ts +45 -0
  147. package/dist/test/fixtures.d.ts.map +1 -0
  148. package/dist/test/fixtures.js +420 -0
  149. package/dist/test/fixtures.js.map +1 -0
  150. package/package.json +2 -1
package/CLAUDE.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # @sudobility/music_types
2
2
 
3
+ > **Git policy — never auto-commit or auto-push.** Leave your work in the working tree.
4
+ > Run `git commit`, `git push`, `gh pr create`, or `scripts/push_all.sh` **only when the user
5
+ > explicitly asks in that turn**. Approval for an earlier change does not carry forward, and
6
+ > finishing a task is not permission to commit it.
7
+
3
8
  TypeScript types and Zod schemas for the ScoreSmith music family (music_api, music_client, music_lib, music_app).
4
9
 
5
10
  ## Tech Stack
@@ -45,3 +50,7 @@ Everything exports from a single sectioned `src/index.ts`:
45
50
  - `music_client` — typed network client + React Query hooks
46
51
  - `music_lib` — domain logic, adapters, store
47
52
  - `music_app` — web app (UI/routing only)
53
+
54
+ ## Git Workflow
55
+
56
+ - Do not use feature branches for code changes. Always stay on the current branch.
@@ -0,0 +1,66 @@
1
+ import type { MusicalEvent, NoteEvent, Score, UUID } from '../../index.js';
2
+ import type { QuantizeOptions } from '../quantization/options.js';
3
+ import type { ScoreCommand } from './types.js';
4
+ export type PasteDestination = {
5
+ trackId: UUID;
6
+ voiceIndex: number;
7
+ anchorTick: number;
8
+ };
9
+ /**
10
+ * Pastes a copied list of note events (e.g. from a prior "copy" of a
11
+ * selection) into `destination.trackId`/`voiceIndex`, anchored so the
12
+ * earliest-starting pasted note lands at `destination.anchorTick` and
13
+ * every other note keeps its relative offset. A pasted note that ends up
14
+ * crossing a measure boundary is split into tied segments, matching
15
+ * `moveNotesCommand`. Ids are regenerated so pasted notes never collide
16
+ * with their source.
17
+ */
18
+ export declare function pasteEventsCommand(notes: NoteEvent[], destination: PasteDestination, label: string): ScoreCommand;
19
+ /**
20
+ * One voice (identified by `trackId`/`measureId`/`voiceId`) touched by a
21
+ * quantize action, with its *note* events already extracted (not the
22
+ * synthetic rests `reflowVoice` fills gaps with — feeding those into
23
+ * `quantizeEvents` alongside real notes could snap a filler rest's start
24
+ * independently of its neighboring note and corrupt the measure). Building
25
+ * this list is O(score) but doesn't itself call `quantizeEvents` — see
26
+ * `collectQuantizeTargets`'s doc comment for why it's split out as its own
27
+ * step (Task 17).
28
+ */
29
+ export type QuantizeTarget = {
30
+ trackId: UUID;
31
+ measureId: UUID;
32
+ voiceId: UUID;
33
+ notes: NoteEvent[];
34
+ };
35
+ /**
36
+ * The cheap, non-quantizing "which voices does this selection touch, and
37
+ * what are their current notes" pass of quantization: walks the score once
38
+ * and collects one `QuantizeTarget` per voice containing at least one of
39
+ * `eventIds`. Split out from `quantizeCommand`'s transform so the
40
+ * intent layer (`editing.ts`'s `quantizeSelection`, `interactions.ts`'s
41
+ * `commitQuantize`) can run this synchronously to decide *how much* work a
42
+ * quantize action actually implies (spec §29: route a manual quantize of
43
+ * >2000 events through `services/quantization/quantize-service.ts`'s
44
+ * worker) before committing to the expensive `quantizeEvents` call either
45
+ * on the main thread or off it — without duplicating this measure/voice
46
+ * walk in two places.
47
+ */
48
+ export declare function collectQuantizeTargets(score: Score, eventIds: readonly UUID[]): QuantizeTarget[];
49
+ /**
50
+ * Splices each target's already-quantized notes (`quantizedByVoiceId`,
51
+ * keyed by `QuantizeTarget.voiceId` — computed by the caller via
52
+ * `quantizeEvents`) back into `score`: clips each note back to its measure's span, then
53
+ * `reflowVoice` regenerates that voice's rests around the result. This
54
+ * keeps quantization measure-local (no cross-measure re-splitting), a
55
+ * deliberate Task 5 scope limitation documented in the original brief.
56
+ * A target whose `voiceId` has no entry in `quantizedByVoiceId` is treated
57
+ * as "nothing computed for it" (splices in an empty note list) — the
58
+ * sole caller (`quantize`, below) always populates every target's key
59
+ * before calling this.
60
+ */
61
+ export declare function applyQuantizedGroups(score: Score, targets: readonly QuantizeTarget[], quantizedByVoiceId: ReadonlyMap<UUID, MusicalEvent[]>): Score;
62
+ /** Quantizes every voice containing at least one of `eventIds`, per Task 4's reusable quantization engine. */
63
+ export declare function quantizeCommand(eventIds: UUID[], options: QuantizeOptions, label: string): ScoreCommand;
64
+ /** Transposes the given notes by `semitones`, re-spelling each per its own measure's key signature. */
65
+ export declare function transposeCommand(eventIds: UUID[], semitones: number, label: string): ScoreCommand;
66
+ //# sourceMappingURL=edit-commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit-commands.d.ts","sourceRoot":"","sources":["../../../src/domain/commands/edit-commands.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,KAAK,EACL,IAAI,EACL,MAAM,gBAAgB,CAAC;AAIxB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,IAAI,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAkCF;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,EAAE,EAClB,WAAW,EAAE,gBAAgB,EAC7B,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAgBD;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,IAAI,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,SAAS,IAAI,EAAE,GACxB,cAAc,EAAE,CAiBlB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,SAAS,cAAc,EAAE,EAClC,kBAAkB,EAAE,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,GACpD,KAAK,CAqCP;AAuBD,8GAA8G;AAC9G,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,IAAI,EAAE,EAChB,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AAuCD,uGAAuG;AACvG,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,IAAI,EAAE,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,YAAY,CAId"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Copy/paste, quantize, and transpose command factories (spec §7, §14,
3
+ * §24). Each factory wraps a pure `(Score) => Score` transform via
4
+ * `transformCommand`.
5
+ */
6
+ import { createId } from '../score/ids.js';
7
+ import { splitNoteAcrossMeasures } from '../score/ties.js';
8
+ import { isNoteEvent } from '../../index.js';
9
+ import { transposePitch } from '../pitch/transpose.js';
10
+ import { quantizeEvents } from '../quantization/quantize.js';
11
+ import { transformCommand } from './snapshot.js';
12
+ import { insertNoteIntoTrack, reflowVoice, withTracks } from './reflow.js';
13
+ function pasteEvents(score, notes, destination) {
14
+ const track = score.tracks.find(t => t.id === destination.trackId);
15
+ if (!track || notes.length === 0)
16
+ return score;
17
+ const originStart = Math.min(...notes.map(n => n.startTick));
18
+ const boundaries = track.measures.map(m => m.startTick);
19
+ let working = track;
20
+ for (const note of notes) {
21
+ const startTick = destination.anchorTick + (note.startTick - originStart);
22
+ const placeholder = {
23
+ ...note,
24
+ id: createId(),
25
+ startTick,
26
+ trackId: destination.trackId,
27
+ tieStart: undefined,
28
+ tieStop: undefined,
29
+ };
30
+ const segments = splitNoteAcrossMeasures(placeholder, boundaries);
31
+ for (const segment of segments) {
32
+ working = insertNoteIntoTrack(working, segment, destination.voiceIndex);
33
+ }
34
+ }
35
+ const tracks = score.tracks.map(t => (t.id === track.id ? working : t));
36
+ return withTracks(score, tracks);
37
+ }
38
+ /**
39
+ * Pastes a copied list of note events (e.g. from a prior "copy" of a
40
+ * selection) into `destination.trackId`/`voiceIndex`, anchored so the
41
+ * earliest-starting pasted note lands at `destination.anchorTick` and
42
+ * every other note keeps its relative offset. A pasted note that ends up
43
+ * crossing a measure boundary is split into tied segments, matching
44
+ * `moveNotesCommand`. Ids are regenerated so pasted notes never collide
45
+ * with their source.
46
+ */
47
+ export function pasteEventsCommand(notes, destination, label) {
48
+ return transformCommand(label, score => pasteEvents(score, notes, destination));
49
+ }
50
+ // ---- quantizeCommand -----------------------------------------------------------
51
+ /** Clips `event` to fit within `measure`'s span, or returns `null` if that leaves no positive duration. */
52
+ function clipToMeasure(event, measureStart, measureEnd) {
53
+ const start = Math.max(event.startTick, measureStart);
54
+ const end = Math.min(event.startTick + event.durationTicks, measureEnd);
55
+ if (end <= start)
56
+ return null;
57
+ return { ...event, startTick: start, durationTicks: end - start };
58
+ }
59
+ /**
60
+ * The cheap, non-quantizing "which voices does this selection touch, and
61
+ * what are their current notes" pass of quantization: walks the score once
62
+ * and collects one `QuantizeTarget` per voice containing at least one of
63
+ * `eventIds`. Split out from `quantizeCommand`'s transform so the
64
+ * intent layer (`editing.ts`'s `quantizeSelection`, `interactions.ts`'s
65
+ * `commitQuantize`) can run this synchronously to decide *how much* work a
66
+ * quantize action actually implies (spec §29: route a manual quantize of
67
+ * >2000 events through `services/quantization/quantize-service.ts`'s
68
+ * worker) before committing to the expensive `quantizeEvents` call either
69
+ * on the main thread or off it — without duplicating this measure/voice
70
+ * walk in two places.
71
+ */
72
+ export function collectQuantizeTargets(score, eventIds) {
73
+ const idSet = new Set(eventIds);
74
+ const targets = [];
75
+ for (const track of score.tracks) {
76
+ for (const measure of track.measures) {
77
+ for (const voice of measure.voices) {
78
+ if (!voice.events.some(e => idSet.has(e.id)))
79
+ continue;
80
+ targets.push({
81
+ trackId: track.id,
82
+ measureId: measure.id,
83
+ voiceId: voice.id,
84
+ notes: voice.events.filter(isNoteEvent),
85
+ });
86
+ }
87
+ }
88
+ }
89
+ return targets;
90
+ }
91
+ /**
92
+ * Splices each target's already-quantized notes (`quantizedByVoiceId`,
93
+ * keyed by `QuantizeTarget.voiceId` — computed by the caller via
94
+ * `quantizeEvents`) back into `score`: clips each note back to its measure's span, then
95
+ * `reflowVoice` regenerates that voice's rests around the result. This
96
+ * keeps quantization measure-local (no cross-measure re-splitting), a
97
+ * deliberate Task 5 scope limitation documented in the original brief.
98
+ * A target whose `voiceId` has no entry in `quantizedByVoiceId` is treated
99
+ * as "nothing computed for it" (splices in an empty note list) — the
100
+ * sole caller (`quantize`, below) always populates every target's key
101
+ * before calling this.
102
+ */
103
+ export function applyQuantizedGroups(score, targets, quantizedByVoiceId) {
104
+ const targetsByTrackMeasure = new Map();
105
+ for (const target of targets) {
106
+ const key = `${target.trackId}::${target.measureId}`;
107
+ const list = targetsByTrackMeasure.get(key);
108
+ if (list)
109
+ list.push(target);
110
+ else
111
+ targetsByTrackMeasure.set(key, [target]);
112
+ }
113
+ const tracks = score.tracks.map(track => {
114
+ const measures = track.measures.map(measure => {
115
+ const measureTargets = targetsByTrackMeasure.get(`${track.id}::${measure.id}`);
116
+ if (!measureTargets)
117
+ return measure;
118
+ let nextMeasure = measure;
119
+ const measureEnd = measure.startTick + measure.durationTicks;
120
+ for (const target of measureTargets) {
121
+ const quantizedNotes = (quantizedByVoiceId.get(target.voiceId) ?? [])
122
+ .map(e => clipToMeasure(e, measure.startTick, measureEnd))
123
+ .filter((e) => e !== null);
124
+ const withQuantized = {
125
+ ...nextMeasure,
126
+ voices: nextMeasure.voices.map(v => v.id === target.voiceId ? { ...v, events: quantizedNotes } : v),
127
+ };
128
+ nextMeasure = reflowVoice(withQuantized, target.voiceId, track.id);
129
+ }
130
+ return nextMeasure;
131
+ });
132
+ const trackChanged = measures.some((m, i) => m !== track.measures[i]);
133
+ return trackChanged ? { ...track, measures } : track;
134
+ });
135
+ return withTracks(score, tracks);
136
+ }
137
+ /**
138
+ * Quantizes the voice(s) containing any of `eventIds`, calling
139
+ * `quantizeEvents` synchronously for every touched voice.
140
+ *
141
+ * This is the only path. A >2000-event selection used to be routed through a
142
+ * worker instead, via a precomputed-groups variant of this command; that was
143
+ * removed once the offload was measured at 0.57ms of saved work against a
144
+ * structured clone of the whole event array in each direction.
145
+ */
146
+ function quantize(score, eventIds, options) {
147
+ const targets = collectQuantizeTargets(score, eventIds);
148
+ const quantizedByVoiceId = new Map(targets.map(t => [t.voiceId, quantizeEvents(t.notes, options)]));
149
+ return applyQuantizedGroups(score, targets, quantizedByVoiceId);
150
+ }
151
+ /** Quantizes every voice containing at least one of `eventIds`, per Task 4's reusable quantization engine. */
152
+ export function quantizeCommand(eventIds, options, label) {
153
+ return transformCommand(label, score => quantize(score, eventIds, options));
154
+ }
155
+ // ---- transposeCommand -----------------------------------------------------------
156
+ function transpose(score, eventIds, semitones) {
157
+ const idSet = new Set(eventIds);
158
+ const tracks = score.tracks.map(track => {
159
+ const measures = track.measures.map(measure => {
160
+ const voices = measure.voices.map(voice => {
161
+ if (!voice.events.some(e => idSet.has(e.id)))
162
+ return voice;
163
+ return {
164
+ ...voice,
165
+ events: voice.events.map(event => isNoteEvent(event) && idSet.has(event.id)
166
+ ? {
167
+ ...event,
168
+ pitch: transposePitch(event.pitch, semitones, measure.keySignature),
169
+ }
170
+ : event),
171
+ };
172
+ });
173
+ const measureChanged = voices.some((v, i) => v !== measure.voices[i]);
174
+ return measureChanged ? { ...measure, voices } : measure;
175
+ });
176
+ const trackChanged = measures.some((m, i) => m !== track.measures[i]);
177
+ return trackChanged ? { ...track, measures } : track;
178
+ });
179
+ return withTracks(score, tracks);
180
+ }
181
+ /** Transposes the given notes by `semitones`, re-spelling each per its own measure's key signature. */
182
+ export function transposeCommand(eventIds, semitones, label) {
183
+ return transformCommand(label, score => transpose(score, eventIds, semitones));
184
+ }
185
+ //# sourceMappingURL=edit-commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit-commands.js","sourceRoot":"","sources":["../../../src/domain/commands/edit-commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import type { Accidental, Articulation, DurationName, Pitch, UUID } from '../../index.js';
2
+ import type { ScoreCommand } from './types.js';
3
+ export type AddNoteParams = {
4
+ trackId: UUID;
5
+ measureId: UUID;
6
+ voiceIndex: number;
7
+ pitch: Pitch;
8
+ startTick: number;
9
+ durationTicks: number;
10
+ velocity?: number;
11
+ articulation?: Articulation;
12
+ };
13
+ /**
14
+ * Adds a new note into `params.measureId`'s voice at `params.voiceIndex`,
15
+ * backfilling rests around it. If the new note overlaps an existing note
16
+ * in that voice, the overlap is resolved deterministically by
17
+ * `reflowVoice`'s "replace-on-overlap" rule (see `reflow.ts`): the new
18
+ * note wins, trimming or dropping whatever it overlaps.
19
+ *
20
+ * Only inserted into `params.measureId` itself: if `startTick +
21
+ * durationTicks` extends past that measure's end, the note is silently
22
+ * truncated to fit (by `reflowVoice`'s measure-clipping), not split into
23
+ * tied segments the way `moveNotesCommand`/`pasteEventsCommand` split a
24
+ * note that crosses a boundary. This is a deliberate, narrower behavior
25
+ * for `addNoteCommand` specifically (an add always targets one named
26
+ * measure, unlike a move/paste's computed destination) — callers that
27
+ * want boundary-spanning insertion should add the note measure-local and
28
+ * then use `moveNotesCommand`/`resizeNotesCommand` if it needs to extend
29
+ * further, or split it into per-measure `addNoteCommand` calls themselves.
30
+ */
31
+ export declare function addNoteCommand(params: AddNoteParams, label: string): ScoreCommand;
32
+ /**
33
+ * Deletes the given note events, backfilling rests so every affected
34
+ * measure stays full. If a deleted note was tied to a partner that isn't
35
+ * also being deleted, that partner's now-dangling tie flag is cleared.
36
+ */
37
+ export declare function deleteEventsCommand(eventIds: UUID[], label: string): ScoreCommand;
38
+ export type MoveNotesParams = {
39
+ deltaTicks: number;
40
+ deltaSemitones: number;
41
+ };
42
+ /**
43
+ * Moves the given notes by `deltaTicks` (clamped within their track's
44
+ * existing tick span) and transposes them by `deltaSemitones`. A note
45
+ * whose new position crosses a measure boundary is split into tied
46
+ * segments (Task 3's `splitNoteAcrossMeasures`); any pre-existing tie
47
+ * flags on the moved note are cleared first, since its former tie
48
+ * partner(s) are no longer necessarily adjacent after the move. If a
49
+ * moved note was tied to a partner that isn't also being moved, that
50
+ * partner's now-dangling tie flag is cleared too.
51
+ */
52
+ export declare function moveNotesCommand(eventIds: UUID[], params: MoveNotesParams, label: string): ScoreCommand;
53
+ /**
54
+ * Sets the given notes' duration to `durationTicks` (a raw tick length,
55
+ * e.g. from a piano-roll drag-resize), clamped to fit within each note's
56
+ * current measure (no cross-measure re-splitting), then reflows.
57
+ */
58
+ export declare function resizeNotesCommand(eventIds: UUID[], durationTicks: number, label: string): ScoreCommand;
59
+ /** Sets the given notes' duration to a named value (e.g. from the inspector), converted to ticks via the score's ppq. */
60
+ export declare function changeDurationCommand(eventIds: UUID[], duration: DurationName, label: string): ScoreCommand;
61
+ /** Sets the given notes' pitch to an absolute `Pitch`. */
62
+ export declare function changePitchCommand(eventIds: UUID[], pitch: Pitch, label: string): ScoreCommand;
63
+ /** Sets the given notes' velocity (0-127). */
64
+ export declare function changeVelocityCommand(eventIds: UUID[], velocity: number, label: string): ScoreCommand;
65
+ /** Sets (or clears, when `articulation` is `undefined`) the given notes' articulation. */
66
+ export declare function changeArticulationCommand(eventIds: UUID[], articulation: Articulation | undefined, label: string): ScoreCommand;
67
+ /** Sets the given notes' pitch accidental, keeping their step/octave. */
68
+ export declare function changeAccidentalCommand(eventIds: UUID[], accidental: Accidental, label: string): ScoreCommand;
69
+ /** Toggles `tieStart` or `tieStop` on the given notes. */
70
+ export declare function toggleTieCommand(eventIds: UUID[], which: 'tieStart' | 'tieStop', label: string): ScoreCommand;
71
+ /** Moves the given notes to the voice at ordinal position `targetVoiceIndex` within their current measure (created if absent). */
72
+ export declare function changeVoiceCommand(eventIds: UUID[], targetVoiceIndex: number, label: string): ScoreCommand;
73
+ //# sourceMappingURL=note-commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"note-commands.d.ts","sourceRoot":"","sources":["../../../src/domain/commands/note-commands.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EAEZ,KAAK,EAGL,IAAI,EACL,MAAM,gBAAgB,CAAC;AAIxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA+C/C,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,IAAI,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAwBF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AAcD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,IAAI,EAAE,EAChB,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AAID,MAAM,MAAM,eAAe,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC;AAiF7E;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,IAAI,EAAE,EAChB,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AA0CD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,IAAI,EAAE,EAChB,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAED,yHAAyH;AACzH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,IAAI,EAAE,EAChB,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAID,0DAA0D;AAC1D,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,IAAI,EAAE,EAChB,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAED,8CAA8C;AAC9C,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,IAAI,EAAE,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAED,0FAA0F;AAC1F,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,IAAI,EAAE,EAChB,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,KAAK,EAAE,MAAM,GACZ,YAAY,CASd;AAED,yEAAyE;AACzE,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,IAAI,EAAE,EAChB,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,MAAM,GACZ,YAAY,CAOd;AAED,0DAA0D;AAC1D,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,IAAI,EAAE,EAChB,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AA0DD,kIAAkI;AAClI,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,IAAI,EAAE,EAChB,gBAAgB,EAAE,MAAM,EACxB,KAAK,EAAE,MAAM,GACZ,YAAY,CAId"}
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Note-editing command factories (spec §7, §14). Each factory returns a
3
+ * `ScoreCommand` built via `transformCommand`, wrapping a pure
4
+ * `(Score) => Score` transform so undo (via Immer patches) is free and
5
+ * structurally guaranteed to be a deep-equal inverse of execute.
6
+ *
7
+ * Every transform here keeps measures valid: `reflowVoice` (see
8
+ * `reflow.ts`) fills gaps with rests and trims overflow after any note is
9
+ * added, removed, resized, or relocated, and `moveNotesCommand` splits a
10
+ * note that crosses a measure boundary into tied segments via Task 3's
11
+ * `splitNoteAcrossMeasures`, per the Task 5 brief.
12
+ */
13
+ import { createId } from '../score/ids.js';
14
+ import { splitNoteAcrossMeasures } from '../score/ties.js';
15
+ import { isNoteEvent } from '../../index.js';
16
+ import { ticksFor } from '../time/ticks.js';
17
+ import { transposePitch } from '../pitch/transpose.js';
18
+ import { transformCommand } from './snapshot.js';
19
+ import { clearDanglingTies, ensureVoiceAtIndex, insertNoteIntoTrack, removeNotesFromTrack, reflowVoice, withTracks, } from './reflow.js';
20
+ // ---- shared traversal helpers ------------------------------------------------
21
+ /**
22
+ * Applies `updater` to every note event in `score` whose id is in
23
+ * `eventIds`; other events pass through unchanged. Preserves referential
24
+ * equality at every level (voice/measure/track) that has no matching
25
+ * event, so a call that matches nothing is a true no-op (see `withTracks`).
26
+ */
27
+ function mapNotes(score, eventIds, updater) {
28
+ const idSet = new Set(eventIds);
29
+ const tracks = score.tracks.map(track => {
30
+ const measures = track.measures.map(measure => {
31
+ const voices = measure.voices.map(voice => {
32
+ if (!voice.events.some(e => idSet.has(e.id)))
33
+ return voice;
34
+ return {
35
+ ...voice,
36
+ events: voice.events.map(event => isNoteEvent(event) && idSet.has(event.id) ? updater(event) : event),
37
+ };
38
+ });
39
+ const measureChanged = voices.some((v, i) => v !== measure.voices[i]);
40
+ return measureChanged ? { ...measure, voices } : measure;
41
+ });
42
+ const trackChanged = measures.some((m, i) => m !== track.measures[i]);
43
+ return trackChanged ? { ...track, measures } : track;
44
+ });
45
+ return withTracks(score, tracks);
46
+ }
47
+ function addNote(score, params) {
48
+ const tracks = score.tracks.map(track => {
49
+ if (track.id !== params.trackId)
50
+ return track;
51
+ const measureExists = track.measures.some(m => m.id === params.measureId);
52
+ if (!measureExists)
53
+ return track;
54
+ const note = {
55
+ id: createId(),
56
+ pitch: params.pitch,
57
+ startTick: params.startTick,
58
+ durationTicks: params.durationTicks,
59
+ velocity: params.velocity ?? 80,
60
+ voiceId: '', // renumbered by insertNoteIntoTrack to the destination voice's id
61
+ trackId: track.id,
62
+ ...(params.articulation ? { articulation: params.articulation } : {}),
63
+ };
64
+ return insertNoteIntoTrack(track, note, params.voiceIndex);
65
+ });
66
+ return withTracks(score, tracks);
67
+ }
68
+ /**
69
+ * Adds a new note into `params.measureId`'s voice at `params.voiceIndex`,
70
+ * backfilling rests around it. If the new note overlaps an existing note
71
+ * in that voice, the overlap is resolved deterministically by
72
+ * `reflowVoice`'s "replace-on-overlap" rule (see `reflow.ts`): the new
73
+ * note wins, trimming or dropping whatever it overlaps.
74
+ *
75
+ * Only inserted into `params.measureId` itself: if `startTick +
76
+ * durationTicks` extends past that measure's end, the note is silently
77
+ * truncated to fit (by `reflowVoice`'s measure-clipping), not split into
78
+ * tied segments the way `moveNotesCommand`/`pasteEventsCommand` split a
79
+ * note that crosses a boundary. This is a deliberate, narrower behavior
80
+ * for `addNoteCommand` specifically (an add always targets one named
81
+ * measure, unlike a move/paste's computed destination) — callers that
82
+ * want boundary-spanning insertion should add the note measure-local and
83
+ * then use `moveNotesCommand`/`resizeNotesCommand` if it needs to extend
84
+ * further, or split it into per-measure `addNoteCommand` calls themselves.
85
+ */
86
+ export function addNoteCommand(params, label) {
87
+ return transformCommand(label, score => addNote(score, params));
88
+ }
89
+ // ---- deleteEventsCommand -------------------------------------------------------
90
+ function deleteEvents(score, eventIds) {
91
+ const idSet = new Set(eventIds);
92
+ // Clear tie flags left dangling on any surviving partner of a deleted
93
+ // note *before* actually removing the notes (clearDanglingTies needs
94
+ // the deleted notes still in place to find their chain partners).
95
+ const detied = clearDanglingTies(score, idSet);
96
+ const tracks = detied.tracks.map(track => removeNotesFromTrack(track, idSet));
97
+ return withTracks(detied, tracks);
98
+ }
99
+ /**
100
+ * Deletes the given note events, backfilling rests so every affected
101
+ * measure stays full. If a deleted note was tied to a partner that isn't
102
+ * also being deleted, that partner's now-dangling tie flag is cleared.
103
+ */
104
+ export function deleteEventsCommand(eventIds, label) {
105
+ return transformCommand(label, score => deleteEvents(score, eventIds));
106
+ }
107
+ function moveNotesOnTrack(track, eventIds, params) {
108
+ const toMove = [];
109
+ track.measures.forEach(measure => {
110
+ measure.voices.forEach((voice, voiceIndex) => {
111
+ voice.events.forEach(event => {
112
+ if (isNoteEvent(event) && eventIds.has(event.id))
113
+ toMove.push({ note: event, voiceIndex });
114
+ });
115
+ });
116
+ });
117
+ if (toMove.length === 0)
118
+ return track;
119
+ let working = removeNotesFromTrack(track, new Set(toMove.map(m => m.note.id)));
120
+ const boundaries = working.measures.map(m => m.startTick);
121
+ const lastMeasure = working.measures[working.measures.length - 1];
122
+ const trackEnd = lastMeasure
123
+ ? lastMeasure.startTick + lastMeasure.durationTicks
124
+ : 0;
125
+ for (const { note, voiceIndex } of toMove) {
126
+ const maxStart = Math.max(0, trackEnd - note.durationTicks);
127
+ const clampedStart = Math.max(0, Math.min(note.startTick + params.deltaTicks, maxStart));
128
+ const destMeasure = working.measures.find(m => clampedStart >= m.startTick &&
129
+ clampedStart < m.startTick + m.durationTicks) ?? lastMeasure;
130
+ const newPitch = params.deltaSemitones !== 0
131
+ ? transposePitch(note.pitch, params.deltaSemitones, destMeasure?.keySignature)
132
+ : note.pitch;
133
+ const movedNote = {
134
+ ...note,
135
+ startTick: clampedStart,
136
+ pitch: newPitch,
137
+ tieStart: undefined,
138
+ tieStop: undefined,
139
+ };
140
+ const segments = splitNoteAcrossMeasures(movedNote, boundaries);
141
+ for (const segment of segments) {
142
+ working = insertNoteIntoTrack(working, segment, voiceIndex);
143
+ }
144
+ }
145
+ return working;
146
+ }
147
+ function moveNotes(score, eventIds, params) {
148
+ const idSet = new Set(eventIds);
149
+ // As in deleteEvents: clear dangling tie flags on any surviving partner
150
+ // of a moved note before moveNotesOnTrack relocates it away.
151
+ const detied = clearDanglingTies(score, idSet);
152
+ const tracks = detied.tracks.map(track => moveNotesOnTrack(track, idSet, params));
153
+ return withTracks(detied, tracks);
154
+ }
155
+ /**
156
+ * Moves the given notes by `deltaTicks` (clamped within their track's
157
+ * existing tick span) and transposes them by `deltaSemitones`. A note
158
+ * whose new position crosses a measure boundary is split into tied
159
+ * segments (Task 3's `splitNoteAcrossMeasures`); any pre-existing tie
160
+ * flags on the moved note are cleared first, since its former tie
161
+ * partner(s) are no longer necessarily adjacent after the move. If a
162
+ * moved note was tied to a partner that isn't also being moved, that
163
+ * partner's now-dangling tie flag is cleared too.
164
+ */
165
+ export function moveNotesCommand(eventIds, params, label) {
166
+ return transformCommand(label, score => moveNotes(score, eventIds, params));
167
+ }
168
+ // ---- resizeNotesCommand / changeDurationCommand ----------------------------------
169
+ function resizeNotes(score, eventIds, durationTicks) {
170
+ const idSet = new Set(eventIds);
171
+ const tracks = score.tracks.map(track => {
172
+ const measures = track.measures.map(measure => {
173
+ let nextMeasure = measure;
174
+ let changed = false;
175
+ for (const voice of measure.voices) {
176
+ if (!voice.events.some(e => idSet.has(e.id)))
177
+ continue;
178
+ changed = true;
179
+ const measureEnd = measure.startTick + measure.durationTicks;
180
+ const updatedNotes = voice.events.filter(isNoteEvent).map(e => {
181
+ if (!idSet.has(e.id))
182
+ return e;
183
+ const clamped = Math.max(1, Math.min(durationTicks, measureEnd - e.startTick));
184
+ return { ...e, durationTicks: clamped };
185
+ });
186
+ const withResized = {
187
+ ...nextMeasure,
188
+ voices: nextMeasure.voices.map(v => v.id === voice.id ? { ...v, events: updatedNotes } : v),
189
+ };
190
+ nextMeasure = reflowVoice(withResized, voice.id, track.id);
191
+ }
192
+ return changed ? nextMeasure : measure;
193
+ });
194
+ const trackChanged = measures.some((m, i) => m !== track.measures[i]);
195
+ return trackChanged ? { ...track, measures } : track;
196
+ });
197
+ return withTracks(score, tracks);
198
+ }
199
+ /**
200
+ * Sets the given notes' duration to `durationTicks` (a raw tick length,
201
+ * e.g. from a piano-roll drag-resize), clamped to fit within each note's
202
+ * current measure (no cross-measure re-splitting), then reflows.
203
+ */
204
+ export function resizeNotesCommand(eventIds, durationTicks, label) {
205
+ return transformCommand(label, score => resizeNotes(score, eventIds, durationTicks));
206
+ }
207
+ /** Sets the given notes' duration to a named value (e.g. from the inspector), converted to ticks via the score's ppq. */
208
+ export function changeDurationCommand(eventIds, duration, label) {
209
+ return transformCommand(label, score => resizeNotes(score, eventIds, ticksFor(duration, score.ppq)));
210
+ }
211
+ // ---- simple per-note field commands ------------------------------------------
212
+ /** Sets the given notes' pitch to an absolute `Pitch`. */
213
+ export function changePitchCommand(eventIds, pitch, label) {
214
+ return transformCommand(label, score => mapNotes(score, eventIds, note => ({ ...note, pitch })));
215
+ }
216
+ /** Sets the given notes' velocity (0-127). */
217
+ export function changeVelocityCommand(eventIds, velocity, label) {
218
+ return transformCommand(label, score => mapNotes(score, eventIds, note => ({ ...note, velocity })));
219
+ }
220
+ /** Sets (or clears, when `articulation` is `undefined`) the given notes' articulation. */
221
+ export function changeArticulationCommand(eventIds, articulation, label) {
222
+ return transformCommand(label, score => mapNotes(score, eventIds, note => {
223
+ if (articulation)
224
+ return { ...note, articulation };
225
+ const updated = { ...note };
226
+ delete updated.articulation;
227
+ return updated;
228
+ }));
229
+ }
230
+ /** Sets the given notes' pitch accidental, keeping their step/octave. */
231
+ export function changeAccidentalCommand(eventIds, accidental, label) {
232
+ return transformCommand(label, score => mapNotes(score, eventIds, note => ({
233
+ ...note,
234
+ pitch: { ...note.pitch, accidental },
235
+ })));
236
+ }
237
+ /** Toggles `tieStart` or `tieStop` on the given notes. */
238
+ export function toggleTieCommand(eventIds, which, label) {
239
+ return transformCommand(label, score => mapNotes(score, eventIds, note => ({ ...note, [which]: !note[which] })));
240
+ }
241
+ // ---- changeVoiceCommand -----------------------------------------------------------
242
+ function changeVoice(score, eventIds, targetVoiceIndex) {
243
+ const idSet = new Set(eventIds);
244
+ const tracks = score.tracks.map(track => {
245
+ const measures = track.measures.map(measure => {
246
+ const matches = [];
247
+ measure.voices.forEach(voice => {
248
+ voice.events.forEach(e => {
249
+ if (isNoteEvent(e) && idSet.has(e.id))
250
+ matches.push(e);
251
+ });
252
+ });
253
+ if (matches.length === 0)
254
+ return measure;
255
+ let nextMeasure = measure;
256
+ for (const voice of measure.voices) {
257
+ if (!voice.events.some(e => idSet.has(e.id)))
258
+ continue;
259
+ const remaining = voice.events
260
+ .filter(isNoteEvent)
261
+ .filter(e => !idSet.has(e.id));
262
+ const withRemoved = {
263
+ ...nextMeasure,
264
+ voices: nextMeasure.voices.map(v => v.id === voice.id ? { ...v, events: remaining } : v),
265
+ };
266
+ nextMeasure = reflowVoice(withRemoved, voice.id, track.id);
267
+ }
268
+ nextMeasure = ensureVoiceAtIndex(nextMeasure, targetVoiceIndex, track.id);
269
+ const targetVoice = nextMeasure.voices[targetVoiceIndex];
270
+ const existingNotes = targetVoice.events.filter(isNoteEvent);
271
+ const movedNotes = matches.map(n => ({
272
+ ...n,
273
+ voiceId: targetVoice.id,
274
+ }));
275
+ nextMeasure = {
276
+ ...nextMeasure,
277
+ voices: nextMeasure.voices.map((v, i) => i === targetVoiceIndex
278
+ ? { ...v, events: [...existingNotes, ...movedNotes] }
279
+ : v),
280
+ };
281
+ return reflowVoice(nextMeasure, targetVoice.id, track.id);
282
+ });
283
+ const trackChanged = measures.some((m, i) => m !== track.measures[i]);
284
+ return trackChanged ? { ...track, measures } : track;
285
+ });
286
+ return withTracks(score, tracks);
287
+ }
288
+ /** Moves the given notes to the voice at ordinal position `targetVoiceIndex` within their current measure (created if absent). */
289
+ export function changeVoiceCommand(eventIds, targetVoiceIndex, label) {
290
+ return transformCommand(label, score => changeVoice(score, eventIds, targetVoiceIndex));
291
+ }
292
+ //# sourceMappingURL=note-commands.js.map