smoosic 1.0.34 → 1.0.35
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.
- package/build/html/smoosic.html +1 -0
- package/build/smoosic.js +36 -14
- package/package.json +1 -1
- package/release/html/smoosic.html +4 -3
- package/release/smoosic.js +36 -14
- package/release/styles/general.css +15 -0
- package/src/application/common.ts +20 -0
- package/src/application/eventHandler.ts +44 -8
- package/src/render/sui/NoteEntryCaret.ts +739 -0
- package/src/render/sui/NoteEntryMediator.ts +58 -0
- package/src/render/sui/mapper.ts +22 -4
- package/src/render/sui/scoreRender.ts +7 -7
- package/src/render/sui/scoreViewOperations.ts +46 -0
- package/src/render/sui/tracker.ts +93 -47
- package/src/render/vex/vxMeasure.ts +2 -1
- package/src/render/vex/vxNote.ts +1 -0
- package/src/smo/data/music.ts +17 -0
- package/src/smo/data/note.ts +3 -1
- package/src/smo/data/noteModifiers.ts +2 -0
- package/src/smo/data/scoreModifiers.ts +0 -3
- package/src/styles/general.css +22 -0
- package/src/ui/components/dialogs/scorePreferences.vue +1 -11
- package/types/src/application/application.d.ts +102 -102
- package/types/src/application/configuration.d.ts +74 -74
- package/types/src/application/dynamicInit.d.ts +1 -1
- package/types/src/application/eventHandler.d.ts +78 -78
- package/types/src/application/exports.d.ts +494 -494
- package/types/src/render/audio/oscillator.d.ts +98 -98
- package/types/src/render/audio/player.d.ts +141 -141
- package/types/src/render/audio/samples.d.ts +56 -56
- package/types/src/render/sui/configuration.d.ts +12 -12
- package/types/src/render/sui/formatter.d.ts +151 -151
- package/types/src/render/sui/layoutDebug.d.ts +43 -43
- package/types/src/render/sui/mapper.d.ts +116 -116
- package/types/src/render/sui/renderState.d.ts +88 -88
- package/types/src/render/sui/scoreRender.d.ts +93 -93
- package/types/src/render/sui/scoreView.d.ts +267 -267
- package/types/src/render/sui/scoreViewOperations.d.ts +594 -594
- package/types/src/render/sui/scroller.d.ts +34 -34
- package/types/src/render/sui/svgPageMap.d.ts +318 -318
- package/types/src/render/sui/textEdit.d.ts +310 -310
- package/types/src/render/vex/vxMeasure.d.ts +95 -95
- package/types/src/smo/data/common.d.ts +220 -220
- package/types/src/smo/data/measure.d.ts +510 -510
- package/types/src/smo/data/measureModifiers.d.ts +506 -506
- package/types/src/smo/data/scoreModifiers.d.ts +433 -433
- package/types/src/smo/xform/selections.d.ts +153 -153
- package/types/src/ui/common.d.ts +45 -45
- package/types/src/ui/configuration.d.ts +31 -31
- package/types/src/ui/dialogs/chordChange.d.ts +35 -35
- package/types/src/ui/dialogs/components/baseComponent.d.ts +158 -158
- package/types/src/ui/dialogs/components/button.d.ts +54 -54
- package/types/src/ui/dialogs/components/dropdown.d.ts +78 -78
- package/types/src/ui/dialogs/components/pitch.d.ts +95 -95
- package/types/src/ui/dialogs/components/rocker.d.ts +66 -66
- package/types/src/ui/dialogs/components/textInPlace.d.ts +90 -90
- package/types/src/ui/dialogs/components/textInput.d.ts +58 -58
- package/types/src/ui/dialogs/components/toggle.d.ts +53 -53
- package/types/src/ui/dialogs/dialog.d.ts +201 -201
- package/types/src/ui/dialogs/endings.d.ts +61 -61
- package/types/src/ui/dialogs/lyric.d.ts +39 -39
- package/types/src/ui/dialogs/measureFormat.d.ts +52 -52
- package/types/src/ui/dialogs/textBlock.d.ts +61 -61
- package/types/src/ui/exceptions.d.ts +12 -12
- package/types/src/ui/menus/manager.d.ts +57 -57
- package/types/src/ui/navigation.d.ts +15 -15
- package/types/typedoc.d.ts +158 -158
- package/release/styles/styles.css +0 -0
- package/src/styles/fonts/Roboto-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/src/styles/fonts/Roboto-VariableFont_wdth,wght.ttf +0 -0
- package/src/styles/styles.css +0 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
import {Pitch, SvgBox, Ticks, Transposable} from "../../smo/data/common";
|
|
2
|
+
import {SuiTracker} from "./tracker";
|
|
3
|
+
import {SmoSelection} from "../../smo/xform/selections";
|
|
4
|
+
import {SvgPage} from "./svgPageMap";
|
|
5
|
+
import {SvgHelpers} from "./svgHelpers";
|
|
6
|
+
import {SmoMeasure} from "../../smo/data/measure";
|
|
7
|
+
import {SmoNote} from "../../smo/data/note";
|
|
8
|
+
import {SmoMusic} from "../../smo/data/music";
|
|
9
|
+
import {SuiScoreViewOperations} from "./scoreViewOperations";
|
|
10
|
+
import {SmoGraceNote} from "../../smo/data/noteModifiers";
|
|
11
|
+
import {Note} from "../../common/vex";
|
|
12
|
+
import {GraceNote, StaveNote} from "vexflow_smoosic";
|
|
13
|
+
import {CaretDelegate} from "./NoteEntryMediator";
|
|
14
|
+
|
|
15
|
+
declare var $: any;
|
|
16
|
+
|
|
17
|
+
type PitchHighlightType = 'selection' | 'drag-init';
|
|
18
|
+
|
|
19
|
+
export class NoteEntryCaret {
|
|
20
|
+
|
|
21
|
+
static readonly STAFF_LINE_HEIGHT = 10;
|
|
22
|
+
static readonly STAFF_LINE_COUNT = 5;
|
|
23
|
+
static readonly STAFF_HEIGHT = NoteEntryCaret.STAFF_LINE_HEIGHT * NoteEntryCaret.STAFF_LINE_COUNT;
|
|
24
|
+
|
|
25
|
+
static readonly LEDGER_POSITIONS_ABOVE = 6;
|
|
26
|
+
static readonly LEDGER_POSITIONS_BELOW = 6;
|
|
27
|
+
|
|
28
|
+
static readonly CARET_HEIGHT = NoteEntryCaret.STAFF_HEIGHT +
|
|
29
|
+
(NoteEntryCaret.LEDGER_POSITIONS_ABOVE * NoteEntryCaret.STAFF_LINE_HEIGHT) +
|
|
30
|
+
(NoteEntryCaret.LEDGER_POSITIONS_BELOW * NoteEntryCaret.STAFF_LINE_HEIGHT);
|
|
31
|
+
|
|
32
|
+
// Highlight colors
|
|
33
|
+
static readonly PITCH_SELECTION_COLOR = '#933';
|
|
34
|
+
static readonly PITCH_DRAG_ORIGINAL_COLOR = '#aaaaaa7f'; // Light grey for the pitch being dragged
|
|
35
|
+
|
|
36
|
+
static readonly VOICE_1_NOTEHEAD_COLOR = '#000000';
|
|
37
|
+
static readonly VOICE_2_NOTEHEAD_COLOR = '#115511';
|
|
38
|
+
static readonly VOICE_3_NOTEHEAD_COLOR = '#555511';
|
|
39
|
+
static readonly VOICE_4_NOTEHEAD_COLOR = '#883344';
|
|
40
|
+
|
|
41
|
+
static readonly VOICE_1_PREVIEW_NOTEHEAD_COLOR = '#808080';
|
|
42
|
+
static readonly VOICE_2_PREVIEW_NOTEHEAD_COLOR = '#88aa88';
|
|
43
|
+
static readonly VOICE_3_PREVIEW_NOTEHEAD_COLOR = '#aaaa88';
|
|
44
|
+
static readonly VOICE_4_PREVIEW_NOTEHEAD_COLOR = '#c499a2';
|
|
45
|
+
|
|
46
|
+
static readonly VOICE_1_CURSOR_RECTANGLE_COLOR = '#99aadd';
|
|
47
|
+
static readonly VOICE_2_CURSOR_RECTANGLE_COLOR = '#99dd99';
|
|
48
|
+
static readonly VOICE_3_CURSOR_RECTANGLE_COLOR = '#dddd99';
|
|
49
|
+
static readonly VOICE_4_CURSOR_RECTANGLE_COLOR = '#dd99aa';
|
|
50
|
+
|
|
51
|
+
//TODO: check if this is needed
|
|
52
|
+
static readonly DEFAULT_NOTE_DURATION: Ticks = { numerator: 4096, denominator: 1, remainder: 0 };
|
|
53
|
+
static readonly DEFAULT_NOTE_MODE: 'note' | 'rest' = 'note';
|
|
54
|
+
|
|
55
|
+
caretBoundaryBox: SvgBox;
|
|
56
|
+
|
|
57
|
+
tracker: SuiTracker;
|
|
58
|
+
view: SuiScoreViewOperations;
|
|
59
|
+
|
|
60
|
+
selection: SmoSelection | null = null;
|
|
61
|
+
note: SmoNote | null = null;
|
|
62
|
+
graceNote: SmoGraceNote | null = null;
|
|
63
|
+
activeNote: Transposable | null = null;
|
|
64
|
+
voice: number = 0;
|
|
65
|
+
vexNoteAbsoluteX: number = 0;
|
|
66
|
+
vexNoteLeftDisplacedHeadPx: number = 0;
|
|
67
|
+
vexNoteRightDisplacedHeadPx: number = 0;
|
|
68
|
+
vexNoteHeadWidth: number = 0;
|
|
69
|
+
vexNoteXShift: number = 0;
|
|
70
|
+
|
|
71
|
+
context: SvgPage | undefined;
|
|
72
|
+
|
|
73
|
+
mode: 'note' | 'rest' = NoteEntryCaret.DEFAULT_NOTE_MODE;
|
|
74
|
+
duration: Ticks = NoteEntryCaret.DEFAULT_NOTE_DURATION;
|
|
75
|
+
|
|
76
|
+
cursorRectangleElement: SVGRectElement | null = null;
|
|
77
|
+
|
|
78
|
+
// Pitch preview
|
|
79
|
+
pitchPreviewElement: SVGTextElement | null = null;
|
|
80
|
+
pitchPreviewLedgerElements: SVGLineElement[] = [];
|
|
81
|
+
|
|
82
|
+
// Pitch selection highlight (persistent)
|
|
83
|
+
pitchSelectionElementId: string | null = null;
|
|
84
|
+
|
|
85
|
+
// Drag-init highlight (cleared on mouse leave)
|
|
86
|
+
pitchDragInitElementId: string | null = null;
|
|
87
|
+
|
|
88
|
+
currentStaffLine: number | null = null;
|
|
89
|
+
occupiedStaffLines: number[] = [];
|
|
90
|
+
|
|
91
|
+
staffLineOnMouseUp: number | null = null;
|
|
92
|
+
staffLineOnMouseDown: number | null = null;
|
|
93
|
+
|
|
94
|
+
delegate: CaretDelegate | null = null;
|
|
95
|
+
|
|
96
|
+
constructor(view: SuiScoreViewOperations, tracker: SuiTracker) {
|
|
97
|
+
this.caretBoundaryBox = SvgHelpers.boxPoints(0, 0, 0, 0);
|
|
98
|
+
this.tracker = tracker;
|
|
99
|
+
this.view = view;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public setSelection(selection: SmoSelection, graceNote: SmoGraceNote | null = null): void {
|
|
103
|
+
this._unsetPreviousSelection();
|
|
104
|
+
const targetVexNote = graceNote ? graceNote.vexGraceNote : selection.note?.vexNote;
|
|
105
|
+
|
|
106
|
+
if (!targetVexNote) {
|
|
107
|
+
console.warn(`${graceNote ? 'Grace note' : 'Note'} does not have coordinates`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.selection = selection;
|
|
112
|
+
this.note = selection.note;
|
|
113
|
+
this.graceNote = graceNote;
|
|
114
|
+
this.activeNote = graceNote ?? selection.note;
|
|
115
|
+
this.voice = selection.selector.voice;
|
|
116
|
+
this.vexNoteAbsoluteX = targetVexNote.getAbsoluteX();
|
|
117
|
+
this.vexNoteHeadWidth = targetVexNote.getMetrics().glyphWidth!;
|
|
118
|
+
this.vexNoteXShift = targetVexNote.getXShift();
|
|
119
|
+
this._calculateDisplacedHeadPosition();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public handleMouseDown(ev: any): void {
|
|
123
|
+
this.staffLineOnMouseUp = null;
|
|
124
|
+
if (!this.selection || this.currentStaffLine === null) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.staffLineOnMouseDown = this.currentStaffLine;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public handleMouseMove(ev: any): void {
|
|
131
|
+
const staffLine = this._calculateStaffLineFromY(ev.clientY);
|
|
132
|
+
if (this.currentStaffLine !== staffLine) {
|
|
133
|
+
this.currentStaffLine = staffLine;
|
|
134
|
+
this._renderPitchPreview(staffLine);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
//drag initiated
|
|
138
|
+
if (this.staffLineOnMouseDown !== null && this.staffLineOnMouseUp === null) {
|
|
139
|
+
const pitchIndex = this._findPitchIndexForStaffLineAndMouseX(this.staffLineOnMouseDown, ev.clientX);
|
|
140
|
+
if (pitchIndex !== -1) {
|
|
141
|
+
// Update visual
|
|
142
|
+
this.renderPitchHighlight(pitchIndex, 'drag-init');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public handleMouseUp(ev: any): void {
|
|
148
|
+
this.staffLineOnMouseUp = this.currentStaffLine;
|
|
149
|
+
|
|
150
|
+
if (this.staffLineOnMouseDown === null || this.staffLineOnMouseUp === null) return;
|
|
151
|
+
|
|
152
|
+
if (this.staffLineOnMouseDown === this.staffLineOnMouseUp) return;
|
|
153
|
+
|
|
154
|
+
if (this.occupiedStaffLines.includes(this.staffLineOnMouseDown) && !this.occupiedStaffLines.includes(this.staffLineOnMouseUp)) {
|
|
155
|
+
//drag and drop detected - update staff lines
|
|
156
|
+
this._removeOccupiedStaffLine(this.staffLineOnMouseDown);
|
|
157
|
+
this._addOccupiedStaffLine(this.staffLineOnMouseUp);
|
|
158
|
+
|
|
159
|
+
//calculate new pitches
|
|
160
|
+
const newPitches = this._getPitchesFromOccupiedStaffLines();
|
|
161
|
+
|
|
162
|
+
//notify via callback
|
|
163
|
+
this.delegate?.onPitchesChanged(newPitches);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public handleMouseClick(ev: any): void {
|
|
168
|
+
if (!this.selection || this.currentStaffLine === null) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Click on existing pitch - select it
|
|
173
|
+
if (this.occupiedStaffLines.includes(this.currentStaffLine) &&
|
|
174
|
+
this.staffLineOnMouseDown === this.staffLineOnMouseUp) {
|
|
175
|
+
const pitchIndex = this._findPitchIndexForStaffLineAndMouseX(this.currentStaffLine, ev.clientX);
|
|
176
|
+
if (pitchIndex !== -1) {
|
|
177
|
+
//notify via callback
|
|
178
|
+
this.delegate?.onPitchClicked(pitchIndex);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Click on the staff line without pitch - add it
|
|
182
|
+
else if (!this.occupiedStaffLines.includes(this.currentStaffLine) &&
|
|
183
|
+
this.staffLineOnMouseDown === this.staffLineOnMouseUp) {
|
|
184
|
+
this._addOccupiedStaffLine(this.currentStaffLine);
|
|
185
|
+
const newPitches = this._getPitchesFromOccupiedStaffLines();
|
|
186
|
+
//notify via callback
|
|
187
|
+
this.delegate?.onPitchesChanged(newPitches);
|
|
188
|
+
|
|
189
|
+
this._unrenderPitchHighlight();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private _calculateDisplacedHeadPosition(): void {
|
|
194
|
+
const vexNote = this._getActiveVexNote();
|
|
195
|
+
if (!vexNote) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const staveNote = vexNote as StaveNote;
|
|
200
|
+
const noteHeads = staveNote.noteHeads;
|
|
201
|
+
|
|
202
|
+
this.vexNoteLeftDisplacedHeadPx = staveNote.getMetrics().leftDisplacedHeadPx;
|
|
203
|
+
this.vexNoteRightDisplacedHeadPx = staveNote.getMetrics().rightDisplacedHeadPx;
|
|
204
|
+
|
|
205
|
+
const hasMultipleHeadsOnSameLine = NoteEntryCaret._hasMultipleNoteHeadsOnSameLine(noteHeads);
|
|
206
|
+
|
|
207
|
+
// When there are two pitches on the same line, vexflow does not set values for notehead displacement.
|
|
208
|
+
// In this case we calculate head displacement manually.
|
|
209
|
+
if (hasMultipleHeadsOnSameLine && this.vexNoteLeftDisplacedHeadPx === 0 && this.vexNoteRightDisplacedHeadPx === 0) {
|
|
210
|
+
if (staveNote.getStemDirection() === 1) {
|
|
211
|
+
//stem up, add right displacement
|
|
212
|
+
this.vexNoteRightDisplacedHeadPx = this.vexNoteHeadWidth;
|
|
213
|
+
} else {
|
|
214
|
+
//stem down, add left displacement
|
|
215
|
+
this.vexNoteLeftDisplacedHeadPx = this.vexNoteHeadWidth;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private _unsetPreviousSelection(): void {
|
|
221
|
+
this.selection = null;
|
|
222
|
+
this.note = null;
|
|
223
|
+
this.graceNote = null;
|
|
224
|
+
this.activeNote = null;
|
|
225
|
+
this.voice = 0;
|
|
226
|
+
this.vexNoteAbsoluteX = 0;
|
|
227
|
+
this.vexNoteLeftDisplacedHeadPx = 0;
|
|
228
|
+
this.vexNoteRightDisplacedHeadPx = 0;
|
|
229
|
+
this.vexNoteHeadWidth = 0;
|
|
230
|
+
this.vexNoteXShift = 0;
|
|
231
|
+
this.staffLineOnMouseUp = null;
|
|
232
|
+
this.staffLineOnMouseDown = null;
|
|
233
|
+
this._unrenderPitchHighlight();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public render(): void {
|
|
237
|
+
this._unrender();
|
|
238
|
+
if (!this.selection || !this.note || !this.activeNote) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this._calculateCaretBoundaryBoxCoordinates(this.selection.measure, this.note!, this.graceNote);
|
|
242
|
+
this._resolveContext();
|
|
243
|
+
this._fillOccupiedStaffLines(this.selection.measure, this.activeNote);
|
|
244
|
+
this._renderCursorRectangleElement();
|
|
245
|
+
|
|
246
|
+
// Render pitch selection highlight if a pitch is selected
|
|
247
|
+
if (this.selection.selector.pitches.length === 1) {
|
|
248
|
+
const pitchIndex = this.selection.selector.pitches[0];
|
|
249
|
+
this.renderPitchHighlight(pitchIndex);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private _calculateCaretBoundaryBoxCoordinates(measure: SmoMeasure, note: SmoNote, graceNote: SmoGraceNote | null): void {
|
|
254
|
+
const staffY = measure.staffY;
|
|
255
|
+
// Calculate top Y: staff Y minus ledger lines above
|
|
256
|
+
const y = staffY - (NoteEntryCaret.LEDGER_POSITIONS_ABOVE * NoteEntryCaret.STAFF_LINE_HEIGHT);
|
|
257
|
+
|
|
258
|
+
this.caretBoundaryBox = SvgHelpers.boxPoints(
|
|
259
|
+
this.vexNoteAbsoluteX - this.vexNoteLeftDisplacedHeadPx + this.vexNoteXShift,
|
|
260
|
+
y,
|
|
261
|
+
this.vexNoteHeadWidth + this.vexNoteLeftDisplacedHeadPx + this.vexNoteRightDisplacedHeadPx,
|
|
262
|
+
NoteEntryCaret.CARET_HEIGHT
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private _resolveContext(): void {
|
|
267
|
+
this.context = this.tracker.renderer.pageMap.getRenderer(this.caretBoundaryBox);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private _fillOccupiedStaffLines(measure: SmoMeasure, note: Transposable) {
|
|
271
|
+
this.occupiedStaffLines = [];
|
|
272
|
+
for (const pitch of note.pitches) {
|
|
273
|
+
//todo: we are ignoring rests. For that to work we need to implement some sort of note type picker
|
|
274
|
+
if (note.noteType === 'n') {
|
|
275
|
+
this.occupiedStaffLines.push(SmoMusic.pitchToStaffLine(measure.clef, pitch));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private _addOccupiedStaffLine(staffLine: number): void {
|
|
281
|
+
if (!this.occupiedStaffLines.includes(staffLine)) {
|
|
282
|
+
this.occupiedStaffLines.push(staffLine);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private _removeOccupiedStaffLine(staffLine: number): void {
|
|
287
|
+
const index = this.occupiedStaffLines.indexOf(staffLine);
|
|
288
|
+
if (index !== -1) {
|
|
289
|
+
this.occupiedStaffLines.splice(index, 1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private _getPitchesFromOccupiedStaffLines(): Pitch[] {
|
|
294
|
+
if (!this.selection) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const pitches: Pitch[] = [];
|
|
299
|
+
const clef = this.selection.measure.clef;
|
|
300
|
+
|
|
301
|
+
for (const staffLine of this.occupiedStaffLines) {
|
|
302
|
+
pitches.push(SmoMusic.staffLineToPitch(clef, staffLine));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return pitches;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private _renderCursorRectangleElement(): void {
|
|
309
|
+
if (this.context) {
|
|
310
|
+
// Adjust coordinates for the page context
|
|
311
|
+
const adjustedY = this.caretBoundaryBox.y - this.context.box.y;
|
|
312
|
+
// Create the cursor rectangle element
|
|
313
|
+
const x = this.vexNoteAbsoluteX + this.vexNoteXShift;
|
|
314
|
+
const y = adjustedY + (NoteEntryCaret.LEDGER_POSITIONS_ABOVE * NoteEntryCaret.STAFF_LINE_HEIGHT) - (NoteEntryCaret.STAFF_LINE_HEIGHT / 2);
|
|
315
|
+
const width = this.vexNoteHeadWidth;
|
|
316
|
+
const height = NoteEntryCaret.STAFF_HEIGHT;
|
|
317
|
+
|
|
318
|
+
const cursorRectangleElement = this._getCursorRectangleElement(x, y, width, height);
|
|
319
|
+
// Add to the correct SVG context - insert at beginning so it appears behind other elements
|
|
320
|
+
this.context.svg.insertBefore(cursorRectangleElement, this.context.svg.firstChild);
|
|
321
|
+
this.cursorRectangleElement = cursorRectangleElement;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private _getCursorRectangleElement(x: number, y: number, width: number, height: number): SVGRectElement {
|
|
326
|
+
const color = this._getVoiceCursorRectangleColor();
|
|
327
|
+
|
|
328
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
329
|
+
rect.setAttribute('x', x.toString());
|
|
330
|
+
rect.setAttribute('y', y.toString());
|
|
331
|
+
rect.setAttribute('width', width.toString());
|
|
332
|
+
rect.setAttribute('height', height.toString());
|
|
333
|
+
rect.setAttribute('stroke', 'none');
|
|
334
|
+
rect.setAttribute('fill', color);
|
|
335
|
+
rect.setAttribute('opacity', '0.5');
|
|
336
|
+
rect.setAttribute('class', 'note-entry-caret');
|
|
337
|
+
|
|
338
|
+
return rect;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
public containsPoint(ev: any): boolean {
|
|
342
|
+
const scrollState = this.tracker?.scroller.scrollState;
|
|
343
|
+
const bb = SvgHelpers.boxPoints(ev.clientX + scrollState.x, ev.clientY + scrollState.y, 1, 1);
|
|
344
|
+
const point = this.tracker.renderer.pageMap.clientToSvg(bb);
|
|
345
|
+
|
|
346
|
+
return SvgHelpers.doesBox1ContainBox2(this.caretBoundaryBox, point);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private _renderPitchPreview(staffLine: number): void {
|
|
350
|
+
this._unrenderPitchPreview();
|
|
351
|
+
|
|
352
|
+
if (this.context !== undefined) {
|
|
353
|
+
//check if staffLine has a pitch on it
|
|
354
|
+
//in case it does, do not render pitch preview
|
|
355
|
+
if (!this.occupiedStaffLines.includes(staffLine)) {
|
|
356
|
+
const staffLineY = this._calculateYFromStaffLine(staffLine);
|
|
357
|
+
const adjustedY = staffLineY - this.context.box.y;
|
|
358
|
+
|
|
359
|
+
const x = this.vexNoteAbsoluteX + this.vexNoteXShift;
|
|
360
|
+
// Determine color based on drag state: black during drag, blue otherwise
|
|
361
|
+
const isDragging = this.staffLineOnMouseDown !== null && this.staffLineOnMouseUp === null;
|
|
362
|
+
|
|
363
|
+
const color = isDragging
|
|
364
|
+
? this._getVoiceNoteheadColor()
|
|
365
|
+
: this._getVoicePreviewColor();
|
|
366
|
+
|
|
367
|
+
// Render ledger lines if needed
|
|
368
|
+
const ledgerPositions = this._getLedgerLinePositions(staffLine);
|
|
369
|
+
for (const ledgerPos of ledgerPositions) {
|
|
370
|
+
const ledgerLine = this._createLedgerLineElement(ledgerPos, color);
|
|
371
|
+
this.context.svg.appendChild(ledgerLine);
|
|
372
|
+
this.pitchPreviewLedgerElements.push(ledgerLine);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Render notehead preview
|
|
376
|
+
const preview = this._getPitchPreviewElement(color);
|
|
377
|
+
preview.setAttribute('x', x.toString());
|
|
378
|
+
preview.setAttribute('y', adjustedY.toString());
|
|
379
|
+
|
|
380
|
+
// Add to the correct SVG context
|
|
381
|
+
this.context.svg.appendChild(preview);
|
|
382
|
+
this.pitchPreviewElement = preview;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private _unrenderPitchPreview(): void {
|
|
388
|
+
if (this.pitchPreviewElement) {
|
|
389
|
+
this.pitchPreviewElement.remove();
|
|
390
|
+
this.pitchPreviewElement = null;
|
|
391
|
+
}
|
|
392
|
+
// Remove ledger lines
|
|
393
|
+
for (const ledger of this.pitchPreviewLedgerElements) {
|
|
394
|
+
ledger.remove();
|
|
395
|
+
}
|
|
396
|
+
this.pitchPreviewLedgerElements = [];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
public renderPitchHighlight(pitchIndex: number, highlightType: PitchHighlightType = 'selection'): void {
|
|
400
|
+
// Only clear the highlight of the type we're about to render
|
|
401
|
+
if (highlightType === 'selection') {
|
|
402
|
+
this._unrenderPitchSelectionHighlight();
|
|
403
|
+
} else {
|
|
404
|
+
this._unrenderPitchDragInitHighlight();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!this.context || !this.activeNote || !this.selection) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (pitchIndex < 0 || pitchIndex >= this.activeNote.pitches.length) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const vexNote = this._getActiveVexNote();
|
|
416
|
+
if (!vexNote) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const staveNote = vexNote as StaveNote;
|
|
421
|
+
const noteHeads = staveNote.noteHeads;
|
|
422
|
+
|
|
423
|
+
// Directly access the notehead by pitch index
|
|
424
|
+
// noteHeads array corresponds to pitches array
|
|
425
|
+
const noteHead = noteHeads[pitchIndex];
|
|
426
|
+
if (!noteHead) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
this._highlightNoteHead(noteHead, highlightType);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private _highlightNoteHead(noteHead: any, highlightType: PitchHighlightType): void {
|
|
434
|
+
const attrs = noteHead.attrs;
|
|
435
|
+
if (attrs?.id) {
|
|
436
|
+
// VexFlow adds 'vf-' prefix to element IDs
|
|
437
|
+
const elementId = `vf-${attrs.id}`;
|
|
438
|
+
const element = document.getElementById(elementId);
|
|
439
|
+
|
|
440
|
+
if (element) {
|
|
441
|
+
// Find the first <path> element inside
|
|
442
|
+
const pathElement = element.querySelector('path');
|
|
443
|
+
if (!pathElement) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Apply highlight color based on type and store element ID
|
|
448
|
+
if (highlightType === 'selection') {
|
|
449
|
+
this.pitchSelectionElementId = elementId;
|
|
450
|
+
// Pitch selection: red color
|
|
451
|
+
pathElement.setAttribute('fill', NoteEntryCaret.PITCH_SELECTION_COLOR);
|
|
452
|
+
pathElement.setAttribute('stroke', NoteEntryCaret.PITCH_SELECTION_COLOR);
|
|
453
|
+
} else {
|
|
454
|
+
this.pitchDragInitElementId = elementId;
|
|
455
|
+
// Drag initiation: light grey color
|
|
456
|
+
pathElement.setAttribute('fill', NoteEntryCaret.PITCH_DRAG_ORIGINAL_COLOR);
|
|
457
|
+
pathElement.setAttribute('stroke', NoteEntryCaret.PITCH_DRAG_ORIGINAL_COLOR);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Unrenders both pitch selection and drag-init highlights
|
|
465
|
+
*/
|
|
466
|
+
private _unrenderPitchHighlight(): void {
|
|
467
|
+
this._unrenderPitchSelectionHighlight();
|
|
468
|
+
this._unrenderPitchDragInitHighlight();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Unrenders only the pitch selection highlight (persistent red highlight)
|
|
473
|
+
*/
|
|
474
|
+
private _unrenderPitchSelectionHighlight(): void {
|
|
475
|
+
if (this.pitchSelectionElementId) {
|
|
476
|
+
const element = document.getElementById(this.pitchSelectionElementId);
|
|
477
|
+
|
|
478
|
+
if (element) {
|
|
479
|
+
const pathElement = element.querySelector('path');
|
|
480
|
+
if (pathElement) {
|
|
481
|
+
const color = this._getVoiceNoteheadColor();
|
|
482
|
+
pathElement.setAttribute('fill', color);
|
|
483
|
+
pathElement.setAttribute('stroke', color);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Clear reference
|
|
488
|
+
this.pitchSelectionElementId = null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Unrenders only the drag-init highlight (temporary highlight during drag)
|
|
494
|
+
*/
|
|
495
|
+
private _unrenderPitchDragInitHighlight(): void {
|
|
496
|
+
if (this.pitchDragInitElementId) {
|
|
497
|
+
const element = document.getElementById(this.pitchDragInitElementId);
|
|
498
|
+
|
|
499
|
+
if (element) {
|
|
500
|
+
const pathElement = element.querySelector('path');
|
|
501
|
+
if (pathElement) {
|
|
502
|
+
const color = this._getVoiceNoteheadColor();
|
|
503
|
+
pathElement.setAttribute('fill', color);
|
|
504
|
+
pathElement.setAttribute('stroke', color);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Clear reference
|
|
509
|
+
this.pitchDragInitElementId = null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private _getPitchPreviewElement(color: string): SVGTextElement {
|
|
514
|
+
// Create pitch preview element - use Unicode note head character
|
|
515
|
+
const preview = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
516
|
+
preview.textContent = '\uE0A4'; // Unicode character U+E0A4
|
|
517
|
+
const fontSize = this.graceNote !== null ? 27 : 41;
|
|
518
|
+
preview.setAttribute('fill', color);
|
|
519
|
+
preview.setAttribute('stroke', color);
|
|
520
|
+
preview.setAttribute('opacity', '1');
|
|
521
|
+
preview.setAttribute('font-family', 'Bravura, serif');
|
|
522
|
+
preview.setAttribute('font-size', fontSize.toString());
|
|
523
|
+
preview.setAttribute('lengthAdjust', 'spacingAndGlyphs');
|
|
524
|
+
|
|
525
|
+
preview.setAttribute('class', 'pitch-preview');
|
|
526
|
+
|
|
527
|
+
return preview;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private _getVoiceNoteheadColor(): string {
|
|
531
|
+
const colors = [
|
|
532
|
+
NoteEntryCaret.VOICE_1_NOTEHEAD_COLOR,
|
|
533
|
+
NoteEntryCaret.VOICE_2_NOTEHEAD_COLOR,
|
|
534
|
+
NoteEntryCaret.VOICE_3_NOTEHEAD_COLOR,
|
|
535
|
+
NoteEntryCaret.VOICE_4_NOTEHEAD_COLOR,
|
|
536
|
+
];
|
|
537
|
+
|
|
538
|
+
return colors[this.voice] ?? NoteEntryCaret.VOICE_1_NOTEHEAD_COLOR;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private _getVoicePreviewColor(): string {
|
|
542
|
+
const colors = [
|
|
543
|
+
NoteEntryCaret.VOICE_1_PREVIEW_NOTEHEAD_COLOR,
|
|
544
|
+
NoteEntryCaret.VOICE_2_PREVIEW_NOTEHEAD_COLOR,
|
|
545
|
+
NoteEntryCaret.VOICE_3_PREVIEW_NOTEHEAD_COLOR,
|
|
546
|
+
NoteEntryCaret.VOICE_4_PREVIEW_NOTEHEAD_COLOR,
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
return colors[this.voice] ?? NoteEntryCaret.VOICE_1_PREVIEW_NOTEHEAD_COLOR;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private _getVoiceCursorRectangleColor(): string {
|
|
553
|
+
const colors = [
|
|
554
|
+
NoteEntryCaret.VOICE_1_CURSOR_RECTANGLE_COLOR,
|
|
555
|
+
NoteEntryCaret.VOICE_2_CURSOR_RECTANGLE_COLOR,
|
|
556
|
+
NoteEntryCaret.VOICE_3_CURSOR_RECTANGLE_COLOR,
|
|
557
|
+
NoteEntryCaret.VOICE_4_CURSOR_RECTANGLE_COLOR,
|
|
558
|
+
];
|
|
559
|
+
|
|
560
|
+
return colors[this.voice] ?? NoteEntryCaret.VOICE_1_PREVIEW_NOTEHEAD_COLOR;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
//todo: vexnote has maxLine and minLine and maybe we should use these properties instead of calculating
|
|
564
|
+
private _getLedgerLinePositions(staffLine: number): number[] {
|
|
565
|
+
const positions: number[] = [];
|
|
566
|
+
|
|
567
|
+
if (staffLine >= 6) {
|
|
568
|
+
// Find highest existing pitch above staff
|
|
569
|
+
const occupiedAbove = this.occupiedStaffLines.filter(l => l >= 6);
|
|
570
|
+
const highestOccupied = occupiedAbove.length > 0 ? Math.max(...occupiedAbove) : 5;
|
|
571
|
+
|
|
572
|
+
// Only draw ledger lines beyond the highest existing pitch
|
|
573
|
+
const startLine = Math.max(6, Math.floor(highestOccupied + 1));
|
|
574
|
+
for (let line = startLine; line <= Math.floor(staffLine); line++) {
|
|
575
|
+
positions.push(line);
|
|
576
|
+
}
|
|
577
|
+
} else if (staffLine <= 0) {
|
|
578
|
+
// Find lowest existing pitch below staff
|
|
579
|
+
const occupiedBelow = this.occupiedStaffLines.filter(l => l <= 0);
|
|
580
|
+
const lowestOccupied = occupiedBelow.length > 0 ? Math.min(...occupiedBelow) : 1;
|
|
581
|
+
|
|
582
|
+
// Only draw ledger lines beyond the lowest existing pitch
|
|
583
|
+
const startLine = Math.min(0, Math.ceil(lowestOccupied - 1));
|
|
584
|
+
for (let line = startLine; line >= Math.ceil(staffLine); line--) {
|
|
585
|
+
positions.push(line);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return positions;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private _createLedgerLineElement(staffLine: number, color: string): SVGLineElement {
|
|
593
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
594
|
+
|
|
595
|
+
const y = this._calculateYFromStaffLine(staffLine) - this.context!.box.y;
|
|
596
|
+
// const x = this.vexNoteAbsoluteX + this.vexNoteXShift;
|
|
597
|
+
|
|
598
|
+
const ledgerLineWidth = 15;
|
|
599
|
+
const x1 = this.vexNoteAbsoluteX + this.vexNoteXShift - 3;
|
|
600
|
+
const x2 = x1 + ledgerLineWidth + 3;
|
|
601
|
+
|
|
602
|
+
line.setAttribute('x1', x1.toString());
|
|
603
|
+
line.setAttribute('y1', y.toString());
|
|
604
|
+
line.setAttribute('x2', x2.toString());
|
|
605
|
+
line.setAttribute('y2', y.toString());
|
|
606
|
+
line.setAttribute('stroke-width', '1.4');
|
|
607
|
+
line.setAttribute('fill', 'none');
|
|
608
|
+
line.setAttribute('stroke', color);
|
|
609
|
+
line.setAttribute('class', 'pitch-preview-ledger');
|
|
610
|
+
|
|
611
|
+
return line;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private _calculateStaffLineFromY(mouseY: number): number {
|
|
615
|
+
const scrollState = this.tracker?.scroller.scrollState;
|
|
616
|
+
const bb = SvgHelpers.boxPoints(0, mouseY + scrollState.y, 1, 1);
|
|
617
|
+
const mouseSvgPoint = this.tracker.renderer.pageMap.clientToSvg(bb);
|
|
618
|
+
|
|
619
|
+
const relativeY = mouseSvgPoint.y - this.caretBoundaryBox.y;
|
|
620
|
+
|
|
621
|
+
const firstLegderBellowStaffY = NoteEntryCaret.LEDGER_POSITIONS_ABOVE * NoteEntryCaret.STAFF_LINE_HEIGHT + NoteEntryCaret.STAFF_HEIGHT;
|
|
622
|
+
|
|
623
|
+
// Calculate staff line position from Y coordinate
|
|
624
|
+
const staffLine = Math.round((firstLegderBellowStaffY - relativeY) / (NoteEntryCaret.STAFF_LINE_HEIGHT / 2)) / 2;
|
|
625
|
+
|
|
626
|
+
return staffLine;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private _calculateYFromStaffLine(staffLine: number): number {
|
|
630
|
+
const firstLegderBellowStaffY = NoteEntryCaret.LEDGER_POSITIONS_ABOVE * NoteEntryCaret.STAFF_LINE_HEIGHT + NoteEntryCaret.STAFF_HEIGHT;
|
|
631
|
+
const relativeY = firstLegderBellowStaffY - (staffLine * NoteEntryCaret.STAFF_LINE_HEIGHT);
|
|
632
|
+
|
|
633
|
+
return relativeY + this.caretBoundaryBox.y;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private _unrender(): void {
|
|
637
|
+
// Restore pitch highlight colors before removing elements
|
|
638
|
+
this._unrenderPitchHighlight();
|
|
639
|
+
|
|
640
|
+
// Remove caret from display
|
|
641
|
+
if (this.cursorRectangleElement) {
|
|
642
|
+
this.cursorRectangleElement.remove();
|
|
643
|
+
this.cursorRectangleElement = null;
|
|
644
|
+
}
|
|
645
|
+
// Also clear pitch preview
|
|
646
|
+
this.resetInteraction();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
public resetInteraction(): void {
|
|
650
|
+
// Clear drag-init highlight when mouse leaves the caret boundary
|
|
651
|
+
// but preserve pitch selection highlight (if any)
|
|
652
|
+
this._unrenderPitchDragInitHighlight();
|
|
653
|
+
this._unrenderPitchPreview();
|
|
654
|
+
this.currentStaffLine = null;
|
|
655
|
+
this.staffLineOnMouseDown = null;
|
|
656
|
+
this.staffLineOnMouseUp = null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private _getActiveVexNote(): Note | GraceNote | null {
|
|
660
|
+
if (this.graceNote) {
|
|
661
|
+
return this.graceNote.vexGraceNote;
|
|
662
|
+
}
|
|
663
|
+
return this.note?.vexNote ?? null;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private static _hasMultipleNoteHeadsOnSameLine(noteHeads: any[]): boolean {
|
|
667
|
+
const staffLineCounts = new Map<number, number>();
|
|
668
|
+
|
|
669
|
+
for (const noteHead of noteHeads) {
|
|
670
|
+
const staffLine = noteHead.getLine();
|
|
671
|
+
const count = (staffLineCounts.get(staffLine) || 0) + 1;
|
|
672
|
+
|
|
673
|
+
// Found two noteheads on the same line
|
|
674
|
+
if (count > 1) {
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
staffLineCounts.set(staffLine, count);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private _findPitchIndexForStaffLineAndMouseX(staffLine: number, mouseX: number): number {
|
|
685
|
+
if (!this.selection || !this.activeNote) {
|
|
686
|
+
return -1;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const vexNote = this._getActiveVexNote();
|
|
690
|
+
if (!vexNote) {
|
|
691
|
+
return -1;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const staveNote = vexNote as StaveNote;
|
|
695
|
+
const noteHeads = staveNote.noteHeads;
|
|
696
|
+
|
|
697
|
+
// Find all noteheads on this staff line
|
|
698
|
+
const matchingNoteHeads: { index: number; noteHead: any; x: number }[] = [];
|
|
699
|
+
|
|
700
|
+
noteHeads.forEach((noteHead, index) => {
|
|
701
|
+
if (noteHead.getLine() === staffLine) {
|
|
702
|
+
// Get the X position of the notehead
|
|
703
|
+
const noteHeadX = noteHead.getAbsoluteX();
|
|
704
|
+
matchingNoteHeads.push({ index, noteHead, x: noteHeadX });
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
if (matchingNoteHeads.length === 0) {
|
|
709
|
+
return -1;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// If only one notehead on this staff line, return it
|
|
713
|
+
if (matchingNoteHeads.length === 1) {
|
|
714
|
+
return matchingNoteHeads[0].index;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Multiple noteheads on same staff line - find which one contains the mouse X
|
|
718
|
+
// Convert mouse clientX to SVG coordinates
|
|
719
|
+
const scrollState = this.tracker?.scroller.scrollState;
|
|
720
|
+
const bb = SvgHelpers.boxPoints(mouseX + scrollState.x, 0, 1, 1);
|
|
721
|
+
const mouseSvgPoint = this.tracker.renderer.pageMap.clientToSvg(bb);
|
|
722
|
+
const mouseSvgX = mouseSvgPoint.x;
|
|
723
|
+
|
|
724
|
+
// Find the notehead that contains the mouse X position
|
|
725
|
+
for (const match of matchingNoteHeads) {
|
|
726
|
+
const noteHeadWidth = match.noteHead.getWidth();
|
|
727
|
+
const noteHeadStartX = match.x;
|
|
728
|
+
const noteHeadEndX = noteHeadStartX + noteHeadWidth;
|
|
729
|
+
|
|
730
|
+
// Check if mouse X is within this notehead's bounds
|
|
731
|
+
if (mouseSvgX >= noteHeadStartX && mouseSvgX <= noteHeadEndX) {
|
|
732
|
+
return match.index;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// If no notehead contains the mouse (clicked between noteheads), return -1
|
|
737
|
+
return -1;
|
|
738
|
+
}
|
|
739
|
+
}
|