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,58 @@
|
|
|
1
|
+
import {SmoSelection} from "../../smo/xform/selections";
|
|
2
|
+
import {SmoGraceNote} from "../../smo/data/noteModifiers";
|
|
3
|
+
import {Pitch} from "../../smo/data/common";
|
|
4
|
+
import {SuiTracker} from "./tracker";
|
|
5
|
+
import {NoteEntryCaret} from "./NoteEntryCaret";
|
|
6
|
+
import {SuiScoreViewOperations} from "./scoreViewOperations";
|
|
7
|
+
|
|
8
|
+
export interface TrackerDelegate {
|
|
9
|
+
onPitchIndexChanged(pitchIndex: number): void;
|
|
10
|
+
onSingleNoteHighlighted(selection: SmoSelection, graceNote: SmoGraceNote | null): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CaretDelegate {
|
|
14
|
+
onPitchClicked(pitchIndex: number): void;
|
|
15
|
+
onPitchesChanged(newPitches: Pitch[]): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class NoteEntryMediator implements TrackerDelegate, CaretDelegate {
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private tracker: SuiTracker,
|
|
22
|
+
private caret: NoteEntryCaret,
|
|
23
|
+
private view: SuiScoreViewOperations
|
|
24
|
+
) {
|
|
25
|
+
this.tracker.delegate = this;
|
|
26
|
+
this.caret.delegate = this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* TrackerDelegate
|
|
31
|
+
*/
|
|
32
|
+
public onPitchIndexChanged(pitchIndex: number) {
|
|
33
|
+
this.caret.renderPitchHighlight(pitchIndex);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public onSingleNoteHighlighted(selection: SmoSelection, graceNote: SmoGraceNote | null) {
|
|
37
|
+
this.caret.setSelection(selection, graceNote);
|
|
38
|
+
this.caret.render();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* CaretDelegate
|
|
43
|
+
*/
|
|
44
|
+
public onPitchClicked(pitchIndex: number) {
|
|
45
|
+
const currentIndex = this.tracker.getPitchIndex();
|
|
46
|
+
if (currentIndex === pitchIndex) {
|
|
47
|
+
this.tracker.clearPitchIndex();
|
|
48
|
+
} else {
|
|
49
|
+
this.tracker.setPitchIndex(pitchIndex);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public onPitchesChanged(newPitches: Pitch[]) {
|
|
54
|
+
this.view.setPitches(newPitches);
|
|
55
|
+
this.tracker.pitchIndex = -1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
}
|
package/src/render/sui/mapper.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { SvgBox } from '../../smo/data/common';
|
|
|
12
12
|
import { SmoNote } from '../../smo/data/note';
|
|
13
13
|
import { SmoScore, SmoModifier } from '../../smo/data/score';
|
|
14
14
|
import { SvgPageMap } from './svgPageMap';
|
|
15
|
+
import { VxMeasure } from '../vex/vxMeasure';
|
|
16
|
+
import {SuiScoreRender} from "./scoreRender";
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* DI information about renderer, so we can notify renderer and it can contain
|
|
@@ -32,7 +34,8 @@ export interface SuiRendererBase {
|
|
|
32
34
|
passState: number,
|
|
33
35
|
renderPromise(): Promise<any>,
|
|
34
36
|
addToReplaceQueue(mm: SmoSelection[]): void,
|
|
35
|
-
renderElement: Element
|
|
37
|
+
renderElement: Element,
|
|
38
|
+
renderer: SuiScoreRender
|
|
36
39
|
}
|
|
37
40
|
/**
|
|
38
41
|
* used to perform highlights in the backgroundd
|
|
@@ -68,6 +71,7 @@ export abstract class SuiMapper {
|
|
|
68
71
|
selectionRects: Record<number, OutlineInfo[]> = {};
|
|
69
72
|
outlines: Record<string, OutlineInfo> = {};
|
|
70
73
|
mapping: boolean = false;
|
|
74
|
+
|
|
71
75
|
constructor(renderer: SuiRendererBase, scroller: SuiScroller) {
|
|
72
76
|
// renderer renders the music when it changes
|
|
73
77
|
this.renderer = renderer;
|
|
@@ -637,19 +641,33 @@ export abstract class SuiMapper {
|
|
|
637
641
|
// const artifacts = SvgHelpers.findIntersectingArtifactFromMap(bb, this.measureNoteMap, SvgHelpers.smoBox(this.scroller.scrollState.scroll));
|
|
638
642
|
// TODO: handle overlapping suggestions
|
|
639
643
|
const modifiers = this.renderer.pageMap.findModifierTabs(logicalBox);
|
|
644
|
+
|
|
640
645
|
if (modifiers.length) {
|
|
646
|
+
// this.eraseMousePositionBox();
|
|
641
647
|
this._setModifierAsSuggestion(modifiers[0]);
|
|
642
|
-
this.eraseMousePositionBox();
|
|
643
648
|
} else if (artifacts.length) {
|
|
644
649
|
const artifact = artifacts[0];
|
|
645
|
-
this.eraseMousePositionBox();
|
|
650
|
+
// this.eraseMousePositionBox();
|
|
646
651
|
this._setArtifactAsSuggestion(artifact);
|
|
647
652
|
} else {
|
|
648
653
|
// no intersection, show mouse hint
|
|
649
|
-
this.createMousePositionBox(logicalBox);
|
|
654
|
+
// this.createMousePositionBox(logicalBox);
|
|
650
655
|
}
|
|
651
656
|
}
|
|
652
657
|
}
|
|
658
|
+
|
|
659
|
+
getIntersectingArtifact(bb: SvgBox): SmoSelection | null {
|
|
660
|
+
const scrollState = this.scroller.scrollState;
|
|
661
|
+
bb = SvgHelpers.boxPoints(bb.x + scrollState.x, bb.y + scrollState.y, bb.width ? bb.width : 1, bb.height ? bb.height : 1);
|
|
662
|
+
const logicalBox = this.renderer.pageMap.clientToSvg(bb);
|
|
663
|
+
const { selections, page } = this.renderer.pageMap.findArtifact(logicalBox);
|
|
664
|
+
if (page && selections.length) {
|
|
665
|
+
return selections[0];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return null
|
|
669
|
+
}
|
|
670
|
+
|
|
653
671
|
_getRectangleChain(selection: SmoSelection) {
|
|
654
672
|
const rv: number[] = [];
|
|
655
673
|
if (!selection.note) {
|
|
@@ -259,14 +259,14 @@ export class SuiScoreRender {
|
|
|
259
259
|
modifier.logicalBox = pageContext.offsetBbox(modifier.element);
|
|
260
260
|
}
|
|
261
261
|
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
262
|
+
// unit test codes don't have tracker.
|
|
263
|
+
if (this.measureMapper) {
|
|
264
|
+
const tmpStaff: SmoSystemStaff | undefined = this.score!.staves.find((ss) => ss.staffId === smoMeasure.measureNumber.staffId);
|
|
265
|
+
if (tmpStaff) {
|
|
266
|
+
this.measureMapper.mapMeasure(tmpStaff, smoMeasure, printing);
|
|
267
|
+
}
|
|
268
268
|
}
|
|
269
|
-
}
|
|
269
|
+
}
|
|
270
270
|
});
|
|
271
271
|
modifiers.forEach((modifier) => {
|
|
272
272
|
if (modifier.element) {
|
|
@@ -35,6 +35,7 @@ import { SuiPiano } from './piano';
|
|
|
35
35
|
import { SvgHelpers } from './svgHelpers';
|
|
36
36
|
import { PromiseHelpers } from '../../common/promiseHelpers';
|
|
37
37
|
import { parseJsonText } from 'typescript';
|
|
38
|
+
import {NoteEntryCaret} from "./NoteEntryCaret";
|
|
38
39
|
declare var $: any;
|
|
39
40
|
declare var SmoConfig: SmoRenderConfiguration;
|
|
40
41
|
|
|
@@ -52,6 +53,10 @@ declare var SmoConfig: SmoRenderConfiguration;
|
|
|
52
53
|
* @category SuiRender
|
|
53
54
|
*/
|
|
54
55
|
export class SuiScoreViewOperations extends SuiScoreView {
|
|
56
|
+
constructor(config: SmoRenderConfiguration, svgContainer: HTMLElement, score: SmoScore, scrollSelector: HTMLElement, undoBuffer: UndoBuffer) {
|
|
57
|
+
super(config, svgContainer, score, scrollSelector, undoBuffer);
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
/**
|
|
56
61
|
* Add a new text group to the score
|
|
57
62
|
* @param textGroup a new text group
|
|
@@ -348,6 +353,17 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
348
353
|
|
|
349
354
|
const altSel = this._getEquivalentSelection(sel);
|
|
350
355
|
|
|
356
|
+
// If this is a chord and a specific pitch is selected, just remove that pitch
|
|
357
|
+
const pitchIndex = this.tracker.getPitchIndex();
|
|
358
|
+
if (sel.note.pitches.length > 1 && pitchIndex >= 0 && pitchIndex < sel.note.pitches.length) {
|
|
359
|
+
sel.note.pitches.splice(pitchIndex, 1);
|
|
360
|
+
altSel!.note!.pitches.splice(pitchIndex, 1);
|
|
361
|
+
SmoNote.sortPitches(sel.note);
|
|
362
|
+
SmoNote.sortPitches(altSel!.note! as SmoNote);
|
|
363
|
+
this.tracker.clearPitchIndex();
|
|
364
|
+
return; // skip rest/hide logic for this selection
|
|
365
|
+
}
|
|
366
|
+
|
|
351
367
|
// set the pitch to be a good position for the rest
|
|
352
368
|
const pitch = JSON.parse(JSON.stringify(
|
|
353
369
|
SmoMeasure.defaultPitchForClef[sel.measure.clef]));
|
|
@@ -1207,6 +1223,36 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
1207
1223
|
await promise;
|
|
1208
1224
|
}
|
|
1209
1225
|
|
|
1226
|
+
async setPitches(pitches: Pitch[]): Promise<void> {
|
|
1227
|
+
const selections = this.tracker.selections;
|
|
1228
|
+
const measureSelections = this.undoTrackerMeasureSelections('set pitches');
|
|
1229
|
+
const graceNotes = this.tracker.getSelectedGraceNotes();
|
|
1230
|
+
if (graceNotes.length === 1 && graceNotes[0].selection !== null && graceNotes[0].selection?.note !== null) {
|
|
1231
|
+
const grace1 = graceNotes[0].modifier as SmoGraceNote;
|
|
1232
|
+
const index = graceNotes[0].selection.note.graceNotes.findIndex((x) => x.attrs.id === grace1.attrs.id);
|
|
1233
|
+
|
|
1234
|
+
const altSel = this._getEquivalentSelection(graceNotes[0].selection);
|
|
1235
|
+
if (altSel && altSel.note !== null) {
|
|
1236
|
+
const grace2 = altSel.note.graceNotes[index];
|
|
1237
|
+
grace2.pitches = pitches;
|
|
1238
|
+
}
|
|
1239
|
+
grace1.pitches = pitches;
|
|
1240
|
+
} else if( selections.length === 1) {
|
|
1241
|
+
const selected = selections[0];
|
|
1242
|
+
SmoOperation.setPitch(selected, pitches);
|
|
1243
|
+
const altSel = this._getEquivalentSelection(selected);
|
|
1244
|
+
SmoOperation.setPitch(altSel!, pitches);
|
|
1245
|
+
if (this.score.preferences.autoAdvance) {
|
|
1246
|
+
// Don't play the next note and the added pitch at the same time.
|
|
1247
|
+
this.tracker.deferNextAutoPlay();
|
|
1248
|
+
this.tracker.moveSelectionRight();
|
|
1249
|
+
}
|
|
1250
|
+
SuiOscillator.playSelectionNow(selected, this.score, 1);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
this._renderChangedMeasures(measureSelections);
|
|
1254
|
+
await this.renderer.updatePromise();
|
|
1255
|
+
}
|
|
1210
1256
|
/**
|
|
1211
1257
|
* Add a pitch to the score at the cursor. This tries to find the best pitch
|
|
1212
1258
|
* to match the letter key (F vs F# for instance) based on key and surrounding notes
|
|
@@ -6,14 +6,22 @@ import { SmoSelection, SmoSelector, ModifierTab } from '../../smo/xform/selectio
|
|
|
6
6
|
import { smoSerialize } from '../../common/serializationHelpers';
|
|
7
7
|
import { SuiOscillator } from '../audio/oscillator';
|
|
8
8
|
import { SmoScore } from '../../smo/data/score';
|
|
9
|
-
import { SvgBox, KeyEvent, defaultKeyEvent, keyHandler } from '../../smo/data/common';
|
|
9
|
+
import { SvgBox, KeyEvent, defaultKeyEvent, keyHandler, Ticks, Pitch, PitchLetter } from '../../smo/data/common';
|
|
10
|
+
import { SmoMusic } from '../../smo/data/music';
|
|
10
11
|
import { SuiScroller } from './scroller';
|
|
11
12
|
import { PasteBuffer } from '../../smo/xform/copypaste';
|
|
12
13
|
import { SmoNote } from '../../smo/data/note';
|
|
13
14
|
import { SmoMeasure } from '../../smo/data/measure';
|
|
14
15
|
import { layoutDebug } from './layoutDebug';
|
|
16
|
+
import { SvgPage } from './svgPageMap';
|
|
17
|
+
import {SmoOperation} from "../../smo/xform/operations";
|
|
18
|
+
import {NoteEntryCaret} from "./NoteEntryCaret";
|
|
19
|
+
import {SmoGraceNote} from "../../smo/data/noteModifiers";
|
|
20
|
+
import {TrackerDelegate} from "./NoteEntryMediator";
|
|
15
21
|
declare var $: any;
|
|
16
22
|
|
|
23
|
+
|
|
24
|
+
|
|
17
25
|
export interface TrackerKeyHandler {
|
|
18
26
|
moveHome : keyHandler,
|
|
19
27
|
moveEnd : keyHandler,
|
|
@@ -39,6 +47,7 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
39
47
|
idleTimer: number = Date.now();
|
|
40
48
|
musicCursorGlyph: SVGSVGElement | null = null;
|
|
41
49
|
deferPlayAdvance: boolean = false;
|
|
50
|
+
delegate: TrackerDelegate | null = null;
|
|
42
51
|
static get strokes(): Record<string, StrokeInfo> {
|
|
43
52
|
return {
|
|
44
53
|
suggestion: {
|
|
@@ -72,7 +81,6 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
72
81
|
strokeDasharray: 0,
|
|
73
82
|
opacity: 1.0
|
|
74
83
|
}
|
|
75
|
-
|
|
76
84
|
};
|
|
77
85
|
}
|
|
78
86
|
constructor(renderer: SuiRendererBase, scroller: SuiScroller) {
|
|
@@ -443,21 +451,48 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
443
451
|
this.idleTimer = Date.now();
|
|
444
452
|
const nselector = JSON.parse(JSON.stringify(this.selections[0].selector));
|
|
445
453
|
nselector.staff = this.score.incrementActiveStaff(offset);
|
|
446
|
-
|
|
454
|
+
|
|
447
455
|
this.selections = [this._getClosestTick(nselector)];
|
|
448
456
|
this.deferHighlight();
|
|
449
457
|
this._createLocalModifiersList();
|
|
450
458
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Set a specific pitch index (called by caret for mouse clicks)
|
|
463
|
+
* This updates the visual pitch selection without modifying data
|
|
464
|
+
*/
|
|
465
|
+
setPitchIndex(index: number): void {
|
|
466
|
+
if (!this.selections.length) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const note = this.selections[0].note as SmoNote;
|
|
471
|
+
if (!note || index < 0 || index >= note.pitches.length) {
|
|
472
|
+
return;
|
|
457
473
|
}
|
|
474
|
+
|
|
475
|
+
// Update the selected pitch index
|
|
476
|
+
this.pitchIndex = index;
|
|
477
|
+
this.selections[0].selector.pitches = [index];
|
|
478
|
+
|
|
479
|
+
// Notify via callback
|
|
480
|
+
this.delegate?.onPitchIndexChanged(index);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
getPitchIndex(): number {
|
|
484
|
+
return this.pitchIndex;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
clearPitchIndex(): void {
|
|
488
|
+
this.pitchIndex = -1;
|
|
489
|
+
if (this.selections.length) {
|
|
490
|
+
this.selections[0].selector.pitches = [];
|
|
491
|
+
}
|
|
492
|
+
// Notify via callback
|
|
493
|
+
this.delegate?.onPitchIndexChanged(-1);
|
|
458
494
|
}
|
|
459
495
|
|
|
460
|
-
// ### _moveSelectionPitch
|
|
461
496
|
// Suggest a specific pitch in a chord, so we can transpose just the one note vs. the whole chord.
|
|
462
497
|
_moveSelectionPitch(index: number) {
|
|
463
498
|
this.idleTimer = Date.now();
|
|
@@ -468,13 +503,16 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
468
503
|
const note = sel.note as SmoNote;
|
|
469
504
|
if (note.pitches.length < 2) {
|
|
470
505
|
this.pitchIndex = -1;
|
|
471
|
-
|
|
506
|
+
// Emit event for pitch index reset
|
|
507
|
+
this.delegate?.onPitchIndexChanged(-1);
|
|
472
508
|
return;
|
|
473
509
|
}
|
|
474
510
|
this.pitchIndex = (this.pitchIndex + index) % note.pitches.length;
|
|
475
511
|
sel.selector.pitches = [];
|
|
476
512
|
sel.selector.pitches.push(this.pitchIndex);
|
|
477
|
-
|
|
513
|
+
|
|
514
|
+
// Notify via callback
|
|
515
|
+
this.delegate?.onPitchIndexChanged(this.pitchIndex);
|
|
478
516
|
}
|
|
479
517
|
moveSelectionPitchUp() {
|
|
480
518
|
this._moveSelectionPitch(1);
|
|
@@ -617,7 +655,7 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
617
655
|
this.highlightQueue.selectionCount = this.selections.length;
|
|
618
656
|
this.deferHighlight();
|
|
619
657
|
}
|
|
620
|
-
selectSuggestion(ev: KeyEvent) {
|
|
658
|
+
selectSuggestion(ev: KeyEvent, deferHighlightSelection: boolean = true) {
|
|
621
659
|
if (!this.suggestion || !this.suggestion.measure || this.score === null) {
|
|
622
660
|
return;
|
|
623
661
|
}
|
|
@@ -648,7 +686,9 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
648
686
|
if (ev.ctrlKey) {
|
|
649
687
|
this._addSelection(this.suggestion);
|
|
650
688
|
this._createLocalModifiersList();
|
|
651
|
-
|
|
689
|
+
if (deferHighlightSelection) {
|
|
690
|
+
this.deferHighlight();
|
|
691
|
+
}
|
|
652
692
|
return;
|
|
653
693
|
}
|
|
654
694
|
if (this.autoPlay) {
|
|
@@ -690,7 +730,9 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
690
730
|
}
|
|
691
731
|
}
|
|
692
732
|
this.score.setActiveStaff(this.selections[0].selector.staff);
|
|
693
|
-
|
|
733
|
+
if (deferHighlightSelection) {
|
|
734
|
+
this.deferHighlight();
|
|
735
|
+
}
|
|
694
736
|
this._createLocalModifiersList();
|
|
695
737
|
}
|
|
696
738
|
_setModifierAsSuggestion(artifact: ModifierTab): void {
|
|
@@ -698,6 +740,9 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
698
740
|
return;
|
|
699
741
|
}
|
|
700
742
|
this.modifierSuggestion = artifact;
|
|
743
|
+
if (artifact.modifier instanceof SmoGraceNote) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
701
746
|
this._drawRect(artifact.box, 'suggestion');
|
|
702
747
|
}
|
|
703
748
|
|
|
@@ -711,14 +756,27 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
711
756
|
break;
|
|
712
757
|
}
|
|
713
758
|
}
|
|
714
|
-
if (
|
|
759
|
+
if (!artifact.box) {
|
|
715
760
|
return;
|
|
716
761
|
}
|
|
762
|
+
|
|
717
763
|
this.modifierSuggestion = null;
|
|
718
764
|
|
|
719
765
|
this.suggestion = artifact;
|
|
766
|
+
// console.log(artifact.box);
|
|
767
|
+
// artifact.box.height = 60;
|
|
768
|
+
if (this.suggestion.note) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
720
771
|
this._drawRect(artifact.box, 'suggestion');
|
|
772
|
+
// if (sameSel) {
|
|
773
|
+
//todo; provide argument to the caret so it knows we are operation on a selected element
|
|
774
|
+
// }
|
|
775
|
+
// const caret = this.getNoteEntryCaret();
|
|
776
|
+
// caret.setSelection(this.suggestion);
|
|
777
|
+
// caret.render();
|
|
721
778
|
}
|
|
779
|
+
|
|
722
780
|
_highlightModifier() {
|
|
723
781
|
let box: SvgBox | null = null;
|
|
724
782
|
if (!this.modifierSelections.length) {
|
|
@@ -726,6 +784,9 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
726
784
|
}
|
|
727
785
|
this.removeModifierSelectionBox();
|
|
728
786
|
this.modifierSelections.forEach((artifact) => {
|
|
787
|
+
if (artifact.modifier instanceof SmoGraceNote) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
729
790
|
if (box === null) {
|
|
730
791
|
box = artifact.modifier.logicalBox ?? null;
|
|
731
792
|
} else {
|
|
@@ -738,19 +799,6 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
738
799
|
this._drawRect(box, 'staffModifier');
|
|
739
800
|
}
|
|
740
801
|
|
|
741
|
-
_highlightPitchSelection(note: SmoNote, index: number) {
|
|
742
|
-
const noteDiv = $(this.renderElement).find('#' + note.renderId);
|
|
743
|
-
const heads = noteDiv.find('.vf-notehead');
|
|
744
|
-
if (!heads.length) {
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
const headEl = heads[index];
|
|
748
|
-
const pageContext = this.renderer.pageMap.getRendererFromModifier(note);
|
|
749
|
-
$(pageContext.svg).find('.vf-pitchSelection').remove();
|
|
750
|
-
const box = pageContext.offsetBbox(headEl);
|
|
751
|
-
this._drawRect(box, 'pitchSelection');
|
|
752
|
-
}
|
|
753
|
-
|
|
754
802
|
_highlightActiveVoice(selection: SmoSelection) {
|
|
755
803
|
let i = 0;
|
|
756
804
|
const selector = selection.selector;
|
|
@@ -775,32 +823,27 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
775
823
|
let prevSel: SmoSelection | null = null;
|
|
776
824
|
let curBox: SvgBox = SvgBox.default;
|
|
777
825
|
this.idleTimer = Date.now();
|
|
778
|
-
const grace = this.getSelectedGraceNotes();
|
|
779
|
-
// If this is not a note with grace notes, logically unselect the grace notes
|
|
780
|
-
if (grace && grace.length && grace[0].selection && this.selections.length) {
|
|
781
|
-
if (!SmoSelector.sameNote(grace[0].selection.selector, this.selections[0].selector)) {
|
|
782
|
-
this.clearModifierSelections();
|
|
783
|
-
} else {
|
|
784
|
-
this._highlightModifier();
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
826
|
// If there is a race condition with a change, avoid referencing null note
|
|
789
827
|
if (!this.selections[0].note) {
|
|
790
828
|
return;
|
|
791
829
|
}
|
|
792
830
|
const note = this.selections[0].note as SmoNote;
|
|
793
|
-
if
|
|
794
|
-
|
|
795
|
-
this.
|
|
796
|
-
this._highlightActiveVoice(this.selections[0]);
|
|
797
|
-
return;
|
|
831
|
+
// Reset pitch index if not in a valid pitch selection scenario
|
|
832
|
+
if (this.pitchIndex < 0 || this.selections.length !== 1 || this.pitchIndex >= note.pitches.length) {
|
|
833
|
+
this.pitchIndex = -1;
|
|
798
834
|
}
|
|
799
|
-
|
|
800
|
-
|
|
835
|
+
|
|
836
|
+
// Single selection - just update voice UI and notify
|
|
801
837
|
if (this.selections.length === 1 && note.logicalBox) {
|
|
802
|
-
this._drawRect(note.logicalBox, 'selection');
|
|
803
838
|
this._highlightActiveVoice(this.selections[0]);
|
|
839
|
+
// Clear any existing multi-selection rectangles
|
|
840
|
+
this.drawSelectionRects([]);
|
|
841
|
+
// Get selected grace note if any
|
|
842
|
+
let graceNote: SmoGraceNote | null = null;
|
|
843
|
+
if (this.modifierSelections.length > 0 && this.modifierSelections[0].modifier instanceof SmoGraceNote) {
|
|
844
|
+
graceNote = this.modifierSelections[0].modifier;
|
|
845
|
+
}
|
|
846
|
+
this.delegate?.onSingleNoteHighlighted(this.selections[0], graceNote);
|
|
804
847
|
return;
|
|
805
848
|
}
|
|
806
849
|
const sorted = this.selections.sort((a, b) => SmoSelector.gt(a.selector, b.selector) ? 1 : -1);
|
|
@@ -901,4 +944,7 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
901
944
|
}
|
|
902
945
|
});
|
|
903
946
|
}
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
|
|
904
950
|
}
|
|
@@ -241,6 +241,7 @@ export class VxMeasure implements VxMeasureIf {
|
|
|
241
241
|
vexNote.setStave(this.stave);
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
|
+
smoNote.vexNote = vexNote;
|
|
244
245
|
const tiedOverPitches = tickIndex === 0 ? this.tiedOverPitches : [];
|
|
245
246
|
const noteData: VexNoteModifierIf = {
|
|
246
247
|
smoMeasure: this.smoMeasure,
|
|
@@ -693,7 +694,7 @@ export class VxMeasure implements VxMeasureIf {
|
|
|
693
694
|
|
|
694
695
|
this.rendered = true;
|
|
695
696
|
if (layoutDebug.mask & layoutDebug.values['adjust']) {
|
|
696
|
-
SvgHelpers.
|
|
697
|
+
SvgHelpers.debugBox(this.context.getContext().svg,
|
|
697
698
|
SvgHelpers.boxPoints(this.dbgLeftX,
|
|
698
699
|
this.smoMeasure.svg.staffY, this.dbgWidth, 40), 'render-x-dbg', 0);
|
|
699
700
|
}
|
package/src/render/vex/vxNote.ts
CHANGED
package/src/smo/data/music.ts
CHANGED
|
@@ -318,6 +318,23 @@ export class SmoMusic {
|
|
|
318
318
|
line += SmoMusic.clefLedgerShift[clef]
|
|
319
319
|
return line;
|
|
320
320
|
}
|
|
321
|
+
static staffLineToPitch(clef: string, line: number): Pitch {
|
|
322
|
+
const adjustedLine = line - SmoMusic.clefLedgerShift[clef];
|
|
323
|
+
const scaledValue = adjustedLine * 2;
|
|
324
|
+
const octaveValue = scaledValue + 28;
|
|
325
|
+
const octave = Math.floor(octaveValue / 7);
|
|
326
|
+
const positionInOctave = ((octaveValue % 7) + 7) % 7;
|
|
327
|
+
|
|
328
|
+
const noteKey = (Object.keys(SmoMusic.noteValues).find((key) =>
|
|
329
|
+
SmoMusic.noteValues[key].root_index === positionInOctave && key.length === 1
|
|
330
|
+
)) as PitchLetter;
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
letter: noteKey,
|
|
334
|
+
accidental: 'n',
|
|
335
|
+
octave
|
|
336
|
+
};
|
|
337
|
+
}
|
|
321
338
|
/**
|
|
322
339
|
* return above if the first pitch is above line 3, else below
|
|
323
340
|
* @param note
|
package/src/smo/data/note.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { SmoMusic } from './music';
|
|
|
16
16
|
import { Ticks, Pitch, SmoAttrs, Transposable, PitchLetter, SvgBox, getId } from './common';
|
|
17
17
|
import { FontInfo, vexCanonicalNotes } from '../../common/vex';
|
|
18
18
|
import { SmoTupletParamsSer } from './tuplet';
|
|
19
|
+
import {Note} from "vexflow_smoosic";
|
|
19
20
|
|
|
20
21
|
export interface PlayedNote {
|
|
21
22
|
pitches: Pitch[],
|
|
@@ -360,12 +361,13 @@ export class SmoNote implements Transposable {
|
|
|
360
361
|
renderId: string | null = null;
|
|
361
362
|
keySignature: string = 'c';
|
|
362
363
|
logicalBox: SvgBox | null = null;
|
|
364
|
+
vexNote: Note | null = null;
|
|
363
365
|
// mixin for real-time audio playback
|
|
364
366
|
audioData: SmoAudioData = {
|
|
365
367
|
volume: [],
|
|
366
368
|
playedNotes: []
|
|
367
369
|
};
|
|
368
|
-
|
|
370
|
+
|
|
369
371
|
isCue: boolean = false;
|
|
370
372
|
hasTabNote: boolean = true;
|
|
371
373
|
accidentalsRendered: string[] = [];// set by renderer if accidental is to display
|
|
@@ -11,6 +11,7 @@ import { SmoAttrs, Ticks, Pitch, getId, SmoObjectParams, Transposable, SvgBox, S
|
|
|
11
11
|
import { smoSerialize } from '../../common/serializationHelpers';
|
|
12
12
|
import { SmoMusic } from './music';
|
|
13
13
|
import { defaultNoteScale, FontInfo, getChordSymbolGlyphFromCode } from '../../common/vex';
|
|
14
|
+
import {GraceNote} from "vexflow_smoosic";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* A note modifier is anything that is mapped to the note, but not part of the
|
|
@@ -220,6 +221,7 @@ export class SmoGraceNote extends SmoNoteModifierBase implements Transposable {
|
|
|
220
221
|
clef: string = 'treble';
|
|
221
222
|
noteType: string = 'n';
|
|
222
223
|
renderId: string | null = null;
|
|
224
|
+
vexGraceNote: GraceNote | null = null;//todo: this is added here to help with EntryCaret
|
|
223
225
|
hasTabNote: boolean = false;
|
|
224
226
|
|
|
225
227
|
tickCount() {
|
|
@@ -144,7 +144,6 @@ export interface SmoScorePreferencesParams {
|
|
|
144
144
|
hideEmptyLines: boolean;
|
|
145
145
|
transposingScore: boolean;
|
|
146
146
|
showPartNames: boolean;
|
|
147
|
-
horizontalDisplay?: boolean;
|
|
148
147
|
}
|
|
149
148
|
/**
|
|
150
149
|
* Some default SMO behavior
|
|
@@ -167,7 +166,6 @@ export class SmoScorePreferences {
|
|
|
167
166
|
autoScrollPlayback: boolean = true;
|
|
168
167
|
transposingScore: boolean = false;
|
|
169
168
|
showPartNames: boolean = false;
|
|
170
|
-
horizontalDisplay: boolean;
|
|
171
169
|
static get defaults(): SmoScorePreferencesParams {
|
|
172
170
|
return {
|
|
173
171
|
autoPlay: true,
|
|
@@ -197,7 +195,6 @@ export class SmoScorePreferences {
|
|
|
197
195
|
this.showPartNames = false;
|
|
198
196
|
}
|
|
199
197
|
}
|
|
200
|
-
this.horizontalDisplay = params.horizontalDisplay ?? false;
|
|
201
198
|
}
|
|
202
199
|
serialize(): SmoScorePreferencesParams {
|
|
203
200
|
return {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.musicContainer {
|
|
2
|
+
user-select: none;
|
|
3
|
+
-webkit-user-select: none;
|
|
4
|
+
-moz-user-select: none;
|
|
5
|
+
-ms-user-select: none;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.pitch-preview {
|
|
9
|
+
cursor: default;
|
|
10
|
+
pointer-events: none;
|
|
11
|
+
/*user-select: none;*/
|
|
12
|
+
/*-webkit-user-select: none;*/
|
|
13
|
+
/*-moz-user-select: none;*/
|
|
14
|
+
/*-ms-user-select: none;*/
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/*.note-entry-caret {*/
|
|
18
|
+
/* user-select: none;*/
|
|
19
|
+
/* -webkit-user-select: none;*/
|
|
20
|
+
/* -moz-user-select: none;*/
|
|
21
|
+
/* -ms-user-select: none;*/
|
|
22
|
+
/*}*/
|
|
@@ -15,8 +15,7 @@ const props: Props = defineProps<Props>();
|
|
|
15
15
|
const { domId, commitCb, cancelCb, getPreferences } = { ...props };
|
|
16
16
|
const preferences = getPreferences();
|
|
17
17
|
|
|
18
|
-
type booleanTypes = 'autoPlay' | 'autoAdvance' | 'showPiano' | 'hideEmptyLines' | 'autoScrollPlayback' | 'transposingScore'
|
|
19
|
-
| 'showPartNames' | 'horizontalDisplay';
|
|
18
|
+
type booleanTypes = 'autoPlay' | 'autoAdvance' | 'showPiano' | 'hideEmptyLines' | 'autoScrollPlayback' | 'transposingScore' | 'showPartNames';
|
|
20
19
|
type numberTypes = 'defaultDupleDuration' | 'defaultTripleDuration';
|
|
21
20
|
|
|
22
21
|
const defaultDoubleDurations: SelectOption[] = [
|
|
@@ -98,15 +97,6 @@ const updateNumber = (type: numberTypes) => {
|
|
|
98
97
|
<label class="form-check-label" :for="getId('partNames')">Show part names in Score</label>
|
|
99
98
|
</div>
|
|
100
99
|
</div>
|
|
101
|
-
<div class="row">
|
|
102
|
-
<div class="col col-1">
|
|
103
|
-
<input class="form-check-input" type="checkbox" v-model="preferences.horizontalDisplay"
|
|
104
|
-
:id="getId('horizontalDisplay')" @change="updateBool('horizontalDisplay')"></input>
|
|
105
|
-
</div>
|
|
106
|
-
<div class="col col-5">
|
|
107
|
-
<label class="form-check-label" :for="getId('hideEmptyLines')">Horizontal Display</label>
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
100
|
<div class="row align-items-baseline mt-3" :id="getId('arp-row')">
|
|
111
101
|
<div class="col col-3 float-end pe-0">
|
|
112
102
|
<span :for="getId('duration-select1')" class="form-label">Default Duration (even meter):</span>
|