smoosic 1.0.21 → 1.0.23
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/README.md +1 -1
- package/build/smoosic.js +22 -33
- package/package.json +4 -4
- package/release/smoosic.js +22 -33
- package/release/styles/images/logo.png +0 -0
- package/release/styles/images/logo_small.png +0 -0
- package/src/application/exports.ts +1 -3
- package/src/render/sui/formatter.ts +2 -1
- package/src/render/sui/scoreRender.ts +18 -4
- package/src/render/sui/scoreView.ts +19 -13
- package/src/render/sui/scoreViewOperations.ts +39 -34
- package/src/render/sui/tracker.ts +34 -29
- package/src/render/vex/vxMeasure.ts +7 -2
- package/src/render/vex/vxNote.ts +16 -1
- package/src/render/vex/vxSystem.ts +2 -1
- package/src/smo/data/music.ts +1 -1
- package/src/smo/data/score.ts +21 -3
- package/src/smo/data/scoreModifiers.ts +11 -4
- package/src/smo/data/systemStaff.ts +39 -6
- package/src/smo/data/tuplet.ts +4 -0
- package/src/smo/xform/copypaste.ts +143 -49
- package/src/smo/xform/tickDuration.ts +4 -0
- package/src/smo/xform/tickMap.ts +1 -28
- package/src/smo/xform/undo.ts +1 -1
- package/src/styles/images/logo.PNG +0 -0
- package/src/styles/images/logo_small.png +0 -0
- package/src/ui/dialogs/dynamics.ts +25 -10
- package/src/ui/dialogs/preferences.ts +11 -0
- package/src/ui/menus/manager.ts +1 -3
- package/src/ui/menus/text.ts +6 -4
- package/typedoc.ts +0 -1
- package/src/styles/images/logo.png +0 -0
- package/src/ui/menus/dynamics.ts +0 -61
|
Binary file
|
|
Binary file
|
|
@@ -104,7 +104,6 @@ import { SuiPartMenu } from '../ui/menus/parts';
|
|
|
104
104
|
import { SuiVoiceMenu } from '../ui/menus/voices';
|
|
105
105
|
import { SuiBeamMenu } from '../ui/menus/beams';
|
|
106
106
|
import { SuiPartSelectionMenu } from '../ui/menus/partSelection';
|
|
107
|
-
import { SuiDynamicsMenu } from '../ui/menus/dynamics';
|
|
108
107
|
import { SuiTimeSignatureMenu } from '../ui/menus/timeSignature';
|
|
109
108
|
import { SuiKeySignatureMenu } from '../ui/menus/keySignature';
|
|
110
109
|
import { SuiStaffModifierMenu } from '../ui/menus/staffModifier';
|
|
@@ -313,7 +312,6 @@ export * from '../ui/i18n/translationEditor';
|
|
|
313
312
|
export * from '../ui/keyBindings/default/editorKeys';
|
|
314
313
|
export * from '../ui/keyBindings/default/trackerKeys';
|
|
315
314
|
export * from '../ui/menus/beams';
|
|
316
|
-
export * from '../ui/menus/dynamics';
|
|
317
315
|
export * from '../ui/menus/file';
|
|
318
316
|
export * from '../ui/menus/keySignature';
|
|
319
317
|
export * from '../ui/menus/language';
|
|
@@ -346,7 +344,7 @@ export const Smo = {
|
|
|
346
344
|
DisplaySettings, ExtendedCollapseParent, CollapseRibbonControl,
|
|
347
345
|
// Menus
|
|
348
346
|
SuiMenuManager, SuiMenuBase, SuiMenuCustomizer, SuiScoreMenu, SuiFileMenu,
|
|
349
|
-
|
|
347
|
+
SuiTimeSignatureMenu, SuiKeySignatureMenu, SuiStaffModifierMenu,
|
|
350
348
|
SuiLanguageMenu, SuiMeasureMenu, SuiNoteMenu, SuiEditMenu, SmoLanguage, SmoTranslator, SuiPartMenu,
|
|
351
349
|
SuiPartSelectionMenu, SuiTextMenu, SuiVoiceMenu, SuiBeamMenu,
|
|
352
350
|
// Dialogs
|
|
@@ -745,7 +745,8 @@ export class SuiLayoutFormatter {
|
|
|
745
745
|
clefLast: string, keySigLast: string, timeSigLast: TimeSignature, tempoLast: SmoTempoText) {
|
|
746
746
|
// The key signature is set based on the transpose index already, i.e. an Eb part in concert C already has 3 sharps.
|
|
747
747
|
const xposeScore = this.score?.preferences?.transposingScore && (this.score?.isPartExposed() === false);
|
|
748
|
-
const xposeOffset = xposeScore ? measure.transposeIndex : 0;
|
|
748
|
+
// const xposeOffset = xposeScore ? measure.transposeIndex : 0;
|
|
749
|
+
const xposeOffset = 0;
|
|
749
750
|
const measureKeySig = SmoMusic.vexKeySignatureTranspose(measure.keySignature, xposeOffset);
|
|
750
751
|
measure.svg.forceClef = (systemIndex === 0 || measure.clef !== clefLast);
|
|
751
752
|
measure.svg.forceTimeSignature = (measure.measureNumber.measureIndex === 0 ||
|
|
@@ -108,10 +108,8 @@ export class SuiScoreRender {
|
|
|
108
108
|
if (newGroup.attachToSelector) {
|
|
109
109
|
// If this text is attached to a staff that is not visible, don't draw it.
|
|
110
110
|
let mappedStaff = this.score!.staves.find((staff) => staff.staffId === newGroup.selector!.staff);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
staff.getMappedStaffId() === newGroup.selector!.staff);
|
|
114
|
-
}
|
|
111
|
+
// We used to translate the staff id here, but this should already be done. The mapped staff of the text group
|
|
112
|
+
// should always match the displayed version of the text group, if this is the view score.
|
|
115
113
|
if (!mappedStaff) {
|
|
116
114
|
return;
|
|
117
115
|
}
|
|
@@ -610,6 +608,22 @@ export class SuiScoreRender {
|
|
|
610
608
|
|
|
611
609
|
measures.forEach((measure) => {
|
|
612
610
|
const context = this.vexContainers.getRenderer(measure.svg.logicalBox);
|
|
611
|
+
// For scores with more than one part, put a helper part number on there.
|
|
612
|
+
if (this.score?.preferences.showPartNames && !printing && !this.score?.isPartExposed()) {
|
|
613
|
+
this.score?.staves.forEach((curStaff) => {
|
|
614
|
+
if (curStaff.partInfo.partAbbreviation.length > 0) {
|
|
615
|
+
const numAr: any[] = [];
|
|
616
|
+
const mm = curStaff.measures[measure.measureNumber.localIndex];
|
|
617
|
+
const modBox = context.offsetSvgPoint(mm.svg.logicalBox);
|
|
618
|
+
numAr.push({ y: modBox.y + mm.svg.logicalBox.height / 2 });
|
|
619
|
+
numAr.push({ x: modBox.x - (20 + curStaff.partInfo.partAbbreviation.length) });
|
|
620
|
+
numAr.push({ 'font-family': SourceSansProFont.fontFamily });
|
|
621
|
+
numAr.push({ 'font-size': '10pt' });
|
|
622
|
+
SvgHelpers.placeSvgText(context.svg, numAr, 'measure-number',
|
|
623
|
+
curStaff.partInfo.partAbbreviation);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
613
627
|
if (measure.measureNumber.localIndex > 0 && measure.measureNumber.systemIndex === 0 && measure.svg.logicalBox && context) {
|
|
614
628
|
const numAr: any[] = [];
|
|
615
629
|
const modBox = context.offsetSvgPoint(measure.svg.logicalBox);
|
|
@@ -617,10 +617,20 @@ export abstract class SuiScoreView {
|
|
|
617
617
|
const row = rows[i];
|
|
618
618
|
if (row.show) {
|
|
619
619
|
const srcStave = this.storeScore.staves[i];
|
|
620
|
-
const jsonObj = srcStave.serialize({ skipMaps: false, preserveIds: true
|
|
620
|
+
const jsonObj = srcStave.serialize({ skipMaps: false, preserveIds: true,
|
|
621
|
+
transposeInstruments: !this.storeScore.preferences.transposingScore
|
|
622
|
+
});
|
|
621
623
|
jsonObj.staffId = staffMap.length;
|
|
622
|
-
const nStave = SmoSystemStaff.deserialize(jsonObj);
|
|
624
|
+
const nStave = SmoSystemStaff.deserialize(jsonObj, !this.storeScore.preferences.transposingScore);
|
|
623
625
|
nStave.mapStaffFromTo(i, nscore.staves.length);
|
|
626
|
+
// Remap score text groups attached to music in this staff
|
|
627
|
+
const localText =
|
|
628
|
+
nscore.textGroups.filter((tt) => (tt.attachToSelector && tt.selector && tt.selector.staff === i));
|
|
629
|
+
localText.forEach((tt) => {
|
|
630
|
+
if (tt.selector) {
|
|
631
|
+
tt.selector.staff = nscore.staves.length;
|
|
632
|
+
}
|
|
633
|
+
})
|
|
624
634
|
nscore.staves.push(nStave);
|
|
625
635
|
if (srcStave.keySignatureMap) {
|
|
626
636
|
nStave.keySignatureMap = JSON.parse(JSON.stringify(srcStave.keySignatureMap));
|
|
@@ -673,13 +683,14 @@ export abstract class SuiScoreView {
|
|
|
673
683
|
this.renderer.setViewport();
|
|
674
684
|
}
|
|
675
685
|
/**
|
|
676
|
-
*
|
|
686
|
+
* If the score is non-transposed, transpose the part so it is in the
|
|
687
|
+
* correct key.
|
|
677
688
|
*/
|
|
678
689
|
_setTransposing() {
|
|
679
|
-
if (
|
|
690
|
+
if (this.isPartExposed()) {
|
|
680
691
|
const xpose = this.score.preferences?.transposingScore;
|
|
681
692
|
if (xpose) {
|
|
682
|
-
this.score.
|
|
693
|
+
this.score.setNonTransposing();
|
|
683
694
|
}
|
|
684
695
|
}
|
|
685
696
|
}
|
|
@@ -727,14 +738,9 @@ export abstract class SuiScoreView {
|
|
|
727
738
|
serialized.keySignature = concertKey;
|
|
728
739
|
const rmeasure = SmoMeasure.deserialize(serialized);
|
|
729
740
|
rmeasure.svg = svg;
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
if (this.score.preferences.transposingScore && !this.score.isPartExposed()) {
|
|
734
|
-
rmeasure.transposeToOffset(-1 * xpose, 'c');
|
|
735
|
-
rmeasure.keySignature = 'c';
|
|
736
|
-
rmeasure.transposeIndex = 0;
|
|
737
|
-
}
|
|
741
|
+
// We used to force a tranposing score stff into 'c' but now we keep the
|
|
742
|
+
// measures in the concert key, and the transpose offset to 0, so no need to do anything
|
|
743
|
+
// special here.
|
|
738
744
|
const selector: SmoSelector = { staff: staffId, measure: i, voice: 0, tick: 0, pitches: [] };
|
|
739
745
|
this.score.replaceMeasure(selector, rmeasure);
|
|
740
746
|
});
|
|
@@ -87,6 +87,7 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
87
87
|
*/
|
|
88
88
|
async removeTextGroup(textGroup: SmoTextGroup): Promise<void> {
|
|
89
89
|
let selector = textGroup.selector ?? SmoSelector.default;
|
|
90
|
+
let altSelector = JSON.parse(JSON.stringify(selector));
|
|
90
91
|
const partInfo = this.score.staves[0].partInfo;
|
|
91
92
|
const isPartExposed = this.isPartExposed();
|
|
92
93
|
const bufType = isPartExposed && partInfo.preserveTextGroups
|
|
@@ -98,14 +99,18 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
98
99
|
if (bufType === UndoBuffer.bufferTypes.PART_MODIFIER) {
|
|
99
100
|
selector.staff = this.staffMap[0];
|
|
100
101
|
} else {
|
|
101
|
-
|
|
102
|
+
altSelector.staff = this.staffMap[selector.staff];
|
|
102
103
|
}
|
|
103
104
|
if (!ogText) {
|
|
104
105
|
return;
|
|
105
106
|
}
|
|
107
|
+
// We undo buffer using the score location, not the group location
|
|
106
108
|
this.storeUndo.addBuffer('remove text group', bufType,
|
|
107
|
-
|
|
109
|
+
altSelector, ogText, UndoBuffer.bufferSubtypes.REMOVE);
|
|
108
110
|
const altGroup = SmoTextGroup.deserializePreserveId(textGroup.serialize());
|
|
111
|
+
if (altGroup.selector) {
|
|
112
|
+
altGroup.selector.staff = altSelector.staff;
|
|
113
|
+
}
|
|
109
114
|
textGroup.elements.forEach((el: ElementLike) => RemoveElementLike(el));
|
|
110
115
|
textGroup.elements = [];
|
|
111
116
|
if (isPartExposed && partInfo.preserveTextGroups) {
|
|
@@ -126,7 +131,8 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
126
131
|
* @returns
|
|
127
132
|
*/
|
|
128
133
|
async updateTextGroup(newVersion: SmoTextGroup): Promise<void> {
|
|
129
|
-
const selector = newVersion.selector ?? SmoSelector.default;
|
|
134
|
+
const selector = newVersion.selector ?? SmoSelector.default; // for score view
|
|
135
|
+
const altSelector = JSON.parse(JSON.stringify(selector)); // for full score
|
|
130
136
|
const isPartExposed = this.isPartExposed();
|
|
131
137
|
const partInfo = this.score.staves[0].partInfo;
|
|
132
138
|
// Back up the original score text
|
|
@@ -144,20 +150,26 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
144
150
|
// if this is part text, make sure the undo buffer is associated with the part stave
|
|
145
151
|
// in the full score, so undo works properly
|
|
146
152
|
if (bufType === UndoBuffer.bufferTypes.PART_MODIFIER) {
|
|
147
|
-
|
|
153
|
+
altSelector.staff = this.staffMap[0];
|
|
148
154
|
} else {
|
|
149
|
-
|
|
155
|
+
altSelector.staff = this.staffMap[selector.staff];
|
|
150
156
|
}
|
|
151
157
|
this.storeUndo.addBuffer('modify text',
|
|
152
|
-
bufType,
|
|
158
|
+
bufType, altSelector, ogtg, UndoBuffer.bufferSubtypes.UPDATE);
|
|
153
159
|
}
|
|
154
160
|
const altNew = SmoTextGroup.deserializePreserveId(newVersion.serialize());
|
|
161
|
+
if (altNew.selector) {
|
|
162
|
+
altNew.selector.staff = altSelector.staff;
|
|
163
|
+
}
|
|
155
164
|
this.score.updateTextGroup(newVersion, true);
|
|
156
|
-
// If this is part text
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
// If this is part text and part-specific text is set,
|
|
166
|
+
// only set the partInfo for the stored score, but don't add to
|
|
167
|
+
// global text.
|
|
168
|
+
if (isPartExposed) {
|
|
169
|
+
const partInfo = this.storeScore.staves[altSelector.staff].partInfo;
|
|
170
|
+
partInfo.updateTextGroup(altNew, true);
|
|
159
171
|
} else {
|
|
160
|
-
this.storeScore.
|
|
172
|
+
this.storeScore.updateTextGroup(altNew, true);
|
|
161
173
|
}
|
|
162
174
|
// TODO: only render the one TG.
|
|
163
175
|
await this.renderer.rerenderTextGroups();
|
|
@@ -224,8 +236,10 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
224
236
|
this.storeScore.updateScorePreferences(new SmoScorePreferences(pref));
|
|
225
237
|
if (curXpose === false && oldXpose === true) {
|
|
226
238
|
this.score.setNonTransposing();
|
|
239
|
+
this.storeScore.setNonTransposing();
|
|
227
240
|
} else if (curXpose === true && oldXpose === false) {
|
|
228
241
|
this.score.setTransposing();
|
|
242
|
+
this.storeScore.setTransposing();
|
|
229
243
|
}
|
|
230
244
|
this.renderer.setDirty();
|
|
231
245
|
return this.renderer.updatePromise();
|
|
@@ -285,37 +299,28 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
285
299
|
SmoOperation.addDynamic(selections[0], dynamic);
|
|
286
300
|
});
|
|
287
301
|
}
|
|
288
|
-
|
|
289
|
-
* Remove dynamics from the selection
|
|
290
|
-
* @param selection
|
|
291
|
-
* @param dynamic
|
|
292
|
-
* @returns
|
|
293
|
-
*/
|
|
294
|
-
async _removeDynamic(selection: SmoSelection, dynamic: SmoDynamicText): Promise<void> {
|
|
295
|
-
const equiv = this._getEquivalentSelection(selection);
|
|
296
|
-
if (equiv !== null && equiv.note !== null) {
|
|
297
|
-
const altModifiers = equiv.note.getModifiers('SmoDynamicText');
|
|
298
|
-
SmoOperation.removeDynamic(selection, dynamic);
|
|
299
|
-
if (altModifiers.length) {
|
|
300
|
-
SmoOperation.removeDynamic(equiv, altModifiers[0] as SmoDynamicText);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
await this.renderer.updatePromise();
|
|
304
|
-
}
|
|
302
|
+
|
|
305
303
|
/**
|
|
306
304
|
* Remove dynamics from the current selection
|
|
307
305
|
* @param dynamic
|
|
308
306
|
* @returns
|
|
309
307
|
*/
|
|
310
308
|
async removeDynamic(dynamic: SmoDynamicText): Promise<void> {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
309
|
+
const measures = SmoSelection.getMeasureList(this.tracker.selections);
|
|
310
|
+
for (let i = 0; i < measures.length; ++i) {
|
|
311
|
+
const selection = measures[i];
|
|
312
|
+
if (selection) {
|
|
313
|
+
const equiv = this._getEquivalentSelection(selection);
|
|
314
|
+
if (equiv?.note) {
|
|
315
|
+
const altModifiers = equiv.note.getModifiers('SmoDynamicText');
|
|
316
|
+
SmoOperation.removeDynamic(selection, dynamic);
|
|
317
|
+
if (altModifiers.length) {
|
|
318
|
+
SmoOperation.removeDynamic(equiv, altModifiers[0] as SmoDynamicText);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
314
322
|
}
|
|
315
|
-
this.
|
|
316
|
-
this._undoFirstMeasureSelection('remove dynamic');
|
|
317
|
-
this._removeDynamic(sel.selection, dynamic);
|
|
318
|
-
this.renderer.addToReplaceQueue(sel.selection);
|
|
323
|
+
this._renderChangedMeasures(measures);
|
|
319
324
|
await this.renderer.updatePromise()
|
|
320
325
|
}
|
|
321
326
|
/**
|
|
@@ -579,35 +579,40 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
579
579
|
this.selections = ar;
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
-
|
|
583
|
-
const selections = SmoSelection.innerSelections(score, sel1.selector, sel2.selector);
|
|
584
|
-
/* .filter((ff) =>
|
|
585
|
-
ff.selector.voice === sel1.measure.activeVoice
|
|
586
|
-
); */
|
|
587
|
-
this.selections = [];
|
|
588
|
-
// Get the actual selections from our map, since the client bounding boxes are already computed
|
|
589
|
-
selections.forEach((sel) => {
|
|
590
|
-
const key = SmoSelector.getNoteKey(sel.selector);
|
|
591
|
-
sel.measure.setActiveVoice(sel.selector.voice);
|
|
592
|
-
// Skip measures that are not rendered because they are part of a multi-rest
|
|
593
|
-
if (this.measureNoteMap && this.measureNoteMap[key]) {
|
|
594
|
-
this.selections.push(this.measureNoteMap[key]);
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
if (this.selections.length === 0) {
|
|
599
|
-
this.selections = [sel1];
|
|
600
|
-
}
|
|
601
|
-
this.idleTimer = Date.now();
|
|
602
|
-
}
|
|
603
|
-
_selectBetweenSelections(s1: SmoSelection, s2: SmoSelection) {
|
|
582
|
+
_selectBetweenSelections(s1o: SmoSelection, s2o: SmoSelection) {
|
|
604
583
|
const score = this.renderer.score ?? null;
|
|
605
584
|
if (!score) {
|
|
606
585
|
return;
|
|
607
586
|
}
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
this.
|
|
587
|
+
const staffMin = Math.min(s1o.selector.staff, s2o.selector.staff);
|
|
588
|
+
const staffMax = Math.max(s1o.selector.staff, s2o.selector.staff);
|
|
589
|
+
this.selections = [];
|
|
590
|
+
for (let i = staffMin; i <= staffMax; ++i) {
|
|
591
|
+
const s1 = JSON.parse(JSON.stringify(s1o.selector));
|
|
592
|
+
s1.staff = i;
|
|
593
|
+
const s2 = JSON.parse(JSON.stringify(s2o.selector));
|
|
594
|
+
s2.staff = i;
|
|
595
|
+
const s1s = SmoSelection.selectionFromSelector(this.score!, s1);
|
|
596
|
+
const s2s = SmoSelection.selectionFromSelector(this.score!, s2);
|
|
597
|
+
if (s1s && s2s) {
|
|
598
|
+
const min = SmoSelector.gt(s1s.selector, s2s.selector) ? s2s : s1s;
|
|
599
|
+
const max = SmoSelector.lt(min.selector, s2s.selector) ? s2s : s1s;
|
|
600
|
+
const selections = SmoSelection.innerSelections(score, min.selector, max.selector);
|
|
601
|
+
// Get the actual selections from our map, since the client bounding boxes are already computed
|
|
602
|
+
selections.forEach((sel) => {
|
|
603
|
+
const key = SmoSelector.getNoteKey(sel.selector);
|
|
604
|
+
sel.measure.setActiveVoice(sel.selector.voice);
|
|
605
|
+
// Skip measures that are not rendered because they are part of a multi-rest
|
|
606
|
+
if (this.measureNoteMap && this.measureNoteMap[key]) {
|
|
607
|
+
this.selections.push(this.measureNoteMap[key]);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
if (this.selections.length === 0) {
|
|
612
|
+
this.selections = [min];
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
611
616
|
this._createLocalModifiersList();
|
|
612
617
|
this.highlightQueue.selectionCount = this.selections.length;
|
|
613
618
|
this.deferHighlight();
|
|
@@ -634,10 +639,10 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
|
634
639
|
|
|
635
640
|
if (ev.shiftKey) {
|
|
636
641
|
const sel1 = this.getExtremeSelection(-1);
|
|
637
|
-
if (sel1.selector.staff === this.suggestion.selector.staff) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
642
|
+
//if (sel1.selector.staff === this.suggestion.selector.staff) {
|
|
643
|
+
this._selectBetweenSelections(sel1, this.suggestion);
|
|
644
|
+
return;
|
|
645
|
+
//}
|
|
641
646
|
}
|
|
642
647
|
|
|
643
648
|
if (ev.ctrlKey) {
|
|
@@ -15,7 +15,7 @@ import { SmoOrnament, SmoDynamicText,
|
|
|
15
15
|
import { SmoSelection } from '../../smo/xform/selections';
|
|
16
16
|
import { SmoMeasure, MeasureTickmaps } from '../../smo/data/measure';
|
|
17
17
|
import { SvgHelpers } from '../sui/svgHelpers';
|
|
18
|
-
import { Clef, IsClef, ElementLike } from '../../smo/data/common';
|
|
18
|
+
import { Clef, IsClef, ElementLike, Pitch } from '../../smo/data/common';
|
|
19
19
|
import { SvgPage } from '../sui/svgPageMap';
|
|
20
20
|
import { SmoTabStave } from '../../smo/data/staffModifiers';
|
|
21
21
|
import { toVexBarlineType, vexBarlineType, vexBarlinePosition, toVexBarlinePosition, toVexSymbol,
|
|
@@ -70,8 +70,10 @@ export class VxMeasure implements VxMeasureIf {
|
|
|
70
70
|
collisionMap: Record<number, SmoNote[]> = {};
|
|
71
71
|
dbgLeftX: number = 0;
|
|
72
72
|
dbgWidth: number = 0;
|
|
73
|
+
tiedOverPitches: Pitch[] = [];
|
|
73
74
|
|
|
74
|
-
constructor(context: SvgPage, selection: SmoSelection,
|
|
75
|
+
constructor(context: SvgPage, selection: SmoSelection,
|
|
76
|
+
printing: boolean, softmax: number, tiedOverPitches: Pitch[]) {
|
|
75
77
|
this.context = context;
|
|
76
78
|
this.rendered = false;
|
|
77
79
|
this.selection = selection;
|
|
@@ -85,6 +87,7 @@ export class VxMeasure implements VxMeasureIf {
|
|
|
85
87
|
this.beamToVexMap = {};
|
|
86
88
|
this.softmax = softmax;
|
|
87
89
|
this.smoTabStave = selection.staff.getTabStaveForMeasure(selection.selector);
|
|
90
|
+
this.tiedOverPitches = tiedOverPitches;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
static get fillStyle() {
|
|
@@ -229,12 +232,14 @@ export class VxMeasure implements VxMeasureIf {
|
|
|
229
232
|
vexNote.setStave(this.stave);
|
|
230
233
|
}
|
|
231
234
|
}
|
|
235
|
+
const tiedOverPitches = tickIndex === 0 ? this.tiedOverPitches : [];
|
|
232
236
|
const noteData: VexNoteModifierIf = {
|
|
233
237
|
smoMeasure: this.smoMeasure,
|
|
234
238
|
vxMeasure: this,
|
|
235
239
|
smoNote: smoNote,
|
|
236
240
|
staveNote: vexNote,
|
|
237
241
|
voiceIndex: voiceIx,
|
|
242
|
+
tiedOverPitches,
|
|
238
243
|
tickIndex: tickIndex
|
|
239
244
|
}
|
|
240
245
|
if (tabNote) {
|
package/src/render/vex/vxNote.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { SmoOrnament, SmoArticulation, SmoDynamicText, SmoLyric,
|
|
|
9
9
|
import { SmoSelection } from '../../smo/xform/selections';
|
|
10
10
|
import { SmoMeasure, MeasureTickmaps } from '../../smo/data/measure';
|
|
11
11
|
import { SvgHelpers } from '../sui/svgHelpers';
|
|
12
|
-
import { Clef, IsClef } from '../../smo/data/common';
|
|
12
|
+
import { Clef, IsClef, Pitch } from '../../smo/data/common';
|
|
13
13
|
import { SvgPage } from '../sui/svgPageMap';
|
|
14
14
|
import { toVexBarlineType, vexBarlineType, vexBarlinePosition, toVexBarlinePosition, toVexSymbol,
|
|
15
15
|
toVexTextJustification, toVexTextPosition, getVexChordBlocks, toVexStemDirection } from './smoAdapter';
|
|
@@ -42,6 +42,7 @@ export interface VexNoteModifierIf {
|
|
|
42
42
|
staveNote: Note,
|
|
43
43
|
voiceIndex: number,
|
|
44
44
|
tickIndex: number,
|
|
45
|
+
tiedOverPitches: Pitch[],
|
|
45
46
|
tabNote?: StemmableNote | TabNote,
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
@@ -96,6 +97,20 @@ export class VxNote {
|
|
|
96
97
|
this.noteData.smoNote.accidentalsRendered = [];
|
|
97
98
|
for (i = 0; i < this.noteData.smoNote.pitches.length && this.noteData.vxMeasure.tickmapObject !== null; ++i) {
|
|
98
99
|
const pitch = this.noteData.smoNote.pitches[i];
|
|
100
|
+
const tiedOver = this.noteData.tiedOverPitches.find((pp) => pp.letter === pitch.letter && pp.octave === pitch.octave);
|
|
101
|
+
if (tiedOver) {
|
|
102
|
+
if (tiedOver.accidental === pitch.accidental) {
|
|
103
|
+
// Why do we do this?
|
|
104
|
+
this.noteData.smoNote.accidentalsRendered.push('');
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const acc = new VF.Accidental(pitch.accidental);
|
|
109
|
+
this.noteData.smoNote.accidentalsRendered.push(pitch.accidental);
|
|
110
|
+
this.noteData.staveNote.addModifier(acc, i);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
99
114
|
const zz = SmoMusic.accidentalDisplay(pitch, this.noteData.smoMeasure.keySignature,
|
|
100
115
|
this.noteData.vxMeasure.tickmapObject.tickmaps[this.noteData.voiceIndex].durationMap[this.noteData.tickIndex],
|
|
101
116
|
this.noteData.vxMeasure.tickmapObject.accidentalArray);
|
|
@@ -621,7 +621,8 @@ export class VxSystem {
|
|
|
621
621
|
if (softmax === SmoMeasureFormat.defaultProportionality) {
|
|
622
622
|
softmax = this.score.layoutManager?.getGlobalLayout().proportionality ?? 0;
|
|
623
623
|
}
|
|
624
|
-
const
|
|
624
|
+
const tiedOverPitches = selection.staff.getTiedPitchesForNextMeasure(smoMeasure.measureNumber.measureIndex - 1);
|
|
625
|
+
const vxMeasure: VxMeasure = new VxMeasure(this.context, selection, printing, softmax, tiedOverPitches);
|
|
625
626
|
|
|
626
627
|
// create the vex notes, beam groups etc. for the measure
|
|
627
628
|
vxMeasure.preFormat();
|
package/src/smo/data/music.ts
CHANGED
|
@@ -592,7 +592,7 @@ export class SmoMusic {
|
|
|
592
592
|
{ letter: 'b', accidental: 'bb', role: 'b5'},
|
|
593
593
|
{ letter: 'b', accidental: 'b', role: '5'},
|
|
594
594
|
{ letter: 'b', accidental: 'n', role: '7/6'},
|
|
595
|
-
{ letter: 'c', accidental: 'b', role: '
|
|
595
|
+
{ letter: 'c', accidental: 'b', role: 'b6'},
|
|
596
596
|
{ letter: 'c', accidental: 'n', role: '6'},
|
|
597
597
|
{ letter: 'c', accidental: '#', role: '7/7'},
|
|
598
598
|
{ letter: 'd', accidental: 'b', role: 'b7'},
|
package/src/smo/data/score.ts
CHANGED
|
@@ -366,7 +366,10 @@ export class SmoScore {
|
|
|
366
366
|
const current = func(measure);
|
|
367
367
|
const ix = measure.measureNumber.measureIndex;
|
|
368
368
|
const currentInstrument = this.staves[0].getStaffInstrument(ix);
|
|
369
|
-
|
|
369
|
+
// If this is a non-transposing score, adjust the key signature
|
|
370
|
+
if (!this.preferences.transposingScore) {
|
|
371
|
+
current.keySignature = SmoMusic.vexKeySigWithOffset(current.keySignature, -1 * currentInstrument.keyOffset);
|
|
372
|
+
}
|
|
370
373
|
if (ix === 0) {
|
|
371
374
|
keySignature[0] = current.keySignature;
|
|
372
375
|
tempo[0] = current.tempo;
|
|
@@ -483,7 +486,9 @@ export class SmoScore {
|
|
|
483
486
|
obj.audioSettings = this.audioSettings.serialize();
|
|
484
487
|
if (!skipStaves) {
|
|
485
488
|
this.staves.forEach((staff: SmoSystemStaff) => {
|
|
486
|
-
obj.staves!.push(staff.serialize({ skipMaps: true, preserveIds: preserveIds
|
|
489
|
+
obj.staves!.push(staff.serialize({ skipMaps: true, preserveIds: preserveIds,
|
|
490
|
+
transposeInstruments: !this.preferences.transposingScore
|
|
491
|
+
}));
|
|
487
492
|
});
|
|
488
493
|
} else {
|
|
489
494
|
obj.staves = [];
|
|
@@ -687,7 +692,7 @@ export class SmoScore {
|
|
|
687
692
|
jsonObj.staves.forEach((staffObj: any, staffIx: number) => {
|
|
688
693
|
staffObj.staffId = staffIx;
|
|
689
694
|
staffObj.renumberingMap = renumberingMap;
|
|
690
|
-
const staff = SmoSystemStaff.deserialize(staffObj);
|
|
695
|
+
const staff = SmoSystemStaff.deserialize(staffObj, !params.preferences?.transposingScore);
|
|
691
696
|
staves.push(staff);
|
|
692
697
|
});
|
|
693
698
|
|
|
@@ -1210,9 +1215,22 @@ export class SmoScore {
|
|
|
1210
1215
|
const tgid = typeof (textGroup) === 'string' ? textGroup :
|
|
1211
1216
|
textGroup.attrs.id;
|
|
1212
1217
|
const ar = this.textGroups.filter((tg) => tg.attrs.id !== tgid);
|
|
1218
|
+
const selector = textGroup.selector;
|
|
1213
1219
|
this.textGroups = ar;
|
|
1214
1220
|
if (toAdd) {
|
|
1215
1221
|
this.textGroups.push(textGroup);
|
|
1222
|
+
// If this is attached to music, push the group to the part
|
|
1223
|
+
if (textGroup.attachToSelector && selector) {
|
|
1224
|
+
const stave = this.staves[selector.staff];
|
|
1225
|
+
if (stave.partInfo.preserveTextGroups) {
|
|
1226
|
+
const partGroup = SmoTextGroup.deserializePreserveId(textGroup);
|
|
1227
|
+
if (partGroup.selector) {
|
|
1228
|
+
partGroup.selector.staff = 0; // TODO: works for 2-staff parts?
|
|
1229
|
+
// Maybe: if (stave.partInfo.stavesAfter) set it to 1.
|
|
1230
|
+
}
|
|
1231
|
+
stave.partInfo.updateTextGroup(partGroup, toAdd);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1216
1234
|
}
|
|
1217
1235
|
}
|
|
1218
1236
|
addTextGroup(textGroup: SmoTextGroup) {
|
|
@@ -124,10 +124,11 @@ export interface SmoScoreInfo {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
export type SmoScorePreferenceBool = 'autoPlay' | 'autoAdvance' | 'showPiano' | 'hideEmptyLines'
|
|
127
|
+
export type SmoScorePreferenceBool = 'autoPlay' | 'autoAdvance' | 'showPiano' | 'hideEmptyLines'
|
|
128
|
+
| 'transposingScore' | 'autoScrollPlayback' | 'showPartNames';
|
|
128
129
|
export type SmoScorePreferenceNumber = 'defaultDupleDuration' | 'defaultTripleDuration';
|
|
129
130
|
export const SmoScorePreferenceBools: SmoScorePreferenceBool[] = ['autoPlay', 'autoAdvance', 'showPiano', 'hideEmptyLines',
|
|
130
|
-
'transposingScore', 'autoScrollPlayback'];
|
|
131
|
+
'transposingScore', 'autoScrollPlayback', 'showPartNames'];
|
|
131
132
|
export const SmoScorePreferenceNumbers: SmoScorePreferenceNumber[] = ['defaultDupleDuration', 'defaultTripleDuration'];
|
|
132
133
|
/**
|
|
133
134
|
* Global score/program behavior preferences, see below for parameters
|
|
@@ -142,6 +143,7 @@ export interface SmoScorePreferencesParams {
|
|
|
142
143
|
showPiano: boolean;
|
|
143
144
|
hideEmptyLines: boolean;
|
|
144
145
|
transposingScore: boolean;
|
|
146
|
+
showPartNames: boolean;
|
|
145
147
|
}
|
|
146
148
|
/**
|
|
147
149
|
* Some default SMO behavior
|
|
@@ -163,6 +165,7 @@ export class SmoScorePreferences {
|
|
|
163
165
|
hideEmptyLines: boolean = false;
|
|
164
166
|
autoScrollPlayback: boolean = true;
|
|
165
167
|
transposingScore: boolean = false;
|
|
168
|
+
showPartNames: boolean = false;
|
|
166
169
|
static get defaults(): SmoScorePreferencesParams {
|
|
167
170
|
return {
|
|
168
171
|
autoPlay: true,
|
|
@@ -172,7 +175,8 @@ export class SmoScorePreferences {
|
|
|
172
175
|
autoScrollPlayback: true,
|
|
173
176
|
showPiano: false,
|
|
174
177
|
hideEmptyLines: false,
|
|
175
|
-
transposingScore: false
|
|
178
|
+
transposingScore: false,
|
|
179
|
+
showPartNames: false
|
|
176
180
|
};
|
|
177
181
|
}
|
|
178
182
|
constructor(params: SmoScorePreferencesParams) {
|
|
@@ -184,9 +188,12 @@ export class SmoScorePreferences {
|
|
|
184
188
|
this[nn] = params[nn];
|
|
185
189
|
});
|
|
186
190
|
// legacy, added later
|
|
187
|
-
if (typeof(params.autoScrollPlayback) === 'undefined') {
|
|
191
|
+
if (typeof(params.autoScrollPlayback) === 'undefined') {
|
|
188
192
|
this.autoScrollPlayback = true;
|
|
189
193
|
}
|
|
194
|
+
if (typeof(params.showPartNames) === 'undefined') {
|
|
195
|
+
this.showPartNames = false;
|
|
196
|
+
}
|
|
190
197
|
}
|
|
191
198
|
}
|
|
192
199
|
serialize(): SmoScorePreferencesParams {
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* @module /smo/data/systemStaff
|
|
7
7
|
* **/
|
|
8
8
|
import { SmoObjectParams, SmoAttrs, MeasureNumber, getId,
|
|
9
|
-
ElementLike } from './common';
|
|
9
|
+
Pitch, ElementLike } from './common';
|
|
10
10
|
import { SmoMusic } from './music';
|
|
11
11
|
import { SmoMeasure, SmoMeasureParamsSer } from './measure';
|
|
12
12
|
import { SmoMeasureFormat, SmoRehearsalMark, SmoRehearsalMarkParams, SmoTempoTextParams, SmoVolta, SmoBarline } from './measureModifiers';
|
|
13
13
|
import { SmoInstrumentParams, StaffModifierBase, SmoInstrument, SmoInstrumentMeasure, SmoInstrumentStringParams, SmoInstrumentNumParams,
|
|
14
14
|
SmoTie, SmoStaffTextBracket, SmoStaffTextBracketParamsSer,
|
|
15
|
-
StaffModifierBaseSer, SmoTabStave, SmoTabStaveParamsSer } from './staffModifiers';
|
|
15
|
+
StaffModifierBaseSer, SmoTabStave, SmoTabStaveParamsSer, TieLine } from './staffModifiers';
|
|
16
16
|
import { SmoPartInfo, SmoPartInfoParamsSer } from './partInfo';
|
|
17
17
|
import { SmoTextGroup } from './scoreText';
|
|
18
18
|
import { SmoSelector } from '../xform/selections';
|
|
@@ -26,7 +26,8 @@ import { FontInfo } from '../../common/vex';
|
|
|
26
26
|
*/
|
|
27
27
|
export interface SmoStaffSerializationOptions {
|
|
28
28
|
skipMaps: boolean,
|
|
29
|
-
preserveIds: boolean
|
|
29
|
+
preserveIds: boolean,
|
|
30
|
+
transposeInstruments: boolean
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
33
|
* Constructor parameters for {@link SmoSystemStaff}.
|
|
@@ -295,7 +296,11 @@ export class SmoSystemStaff implements SmoObjectParams {
|
|
|
295
296
|
params.measureInstrumentMap![parseInt(ikey, 10)] = this.measureInstrumentMap[parseInt(ikey, 10)].serialize();
|
|
296
297
|
});
|
|
297
298
|
this.measures.forEach((measure) => {
|
|
298
|
-
|
|
299
|
+
const mp = measure.serialize();
|
|
300
|
+
if (!options.transposeInstruments) {
|
|
301
|
+
mp.transposeIndex = 0;
|
|
302
|
+
}
|
|
303
|
+
params.measures!.push(mp);
|
|
299
304
|
});
|
|
300
305
|
params.modifiers = [];
|
|
301
306
|
this.modifiers.forEach((modifier) => {
|
|
@@ -314,7 +319,7 @@ export class SmoSystemStaff implements SmoObjectParams {
|
|
|
314
319
|
}
|
|
315
320
|
// ### deserialize
|
|
316
321
|
// parse formerly serialized staff.
|
|
317
|
-
static deserialize(jsonObj: SmoSystemStaffParamsSer): SmoSystemStaff {
|
|
322
|
+
static deserialize(jsonObj: SmoSystemStaffParamsSer, transposeInstruments: boolean): SmoSystemStaff {
|
|
318
323
|
const params: SmoSystemStaffParams = SmoSystemStaff.defaults;
|
|
319
324
|
params.staffId = jsonObj.staffId ?? 0;
|
|
320
325
|
params.measures = [];
|
|
@@ -386,7 +391,9 @@ export class SmoSystemStaff implements SmoObjectParams {
|
|
|
386
391
|
instrumentAr[curInstrumentIndex + 1].measureIndex) {
|
|
387
392
|
curInstrumentIndex += 1;
|
|
388
393
|
}
|
|
389
|
-
|
|
394
|
+
if (transposeInstruments) {
|
|
395
|
+
measure.transposeIndex = instrumentAr[curInstrumentIndex].instrument.keyOffset;
|
|
396
|
+
}
|
|
390
397
|
measure.lines = instrumentAr[curInstrumentIndex].instrument.lines;
|
|
391
398
|
params.measures.push(measure);
|
|
392
399
|
});
|
|
@@ -581,6 +588,32 @@ export class SmoSystemStaff implements SmoObjectParams {
|
|
|
581
588
|
this.removeTabStaves(toRemove);
|
|
582
589
|
this.tabStaves.push(ts);
|
|
583
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* Get all the pitches that start ties to the next measure, so that their
|
|
593
|
+
* accidentals may be preserved
|
|
594
|
+
* @param selector
|
|
595
|
+
* @returns
|
|
596
|
+
*/
|
|
597
|
+
getTiedPitchesForNextMeasure(measureIndex: number) {
|
|
598
|
+
const rv: Pitch[] = [];
|
|
599
|
+
if (measureIndex <= 0) {
|
|
600
|
+
return rv;
|
|
601
|
+
}
|
|
602
|
+
for (let i = 0; i < this.measures[measureIndex].voices.length; ++i) {
|
|
603
|
+
const voice = this.measures[measureIndex].voices[i];
|
|
604
|
+
const lastNote = voice.notes[voice.notes.length - 1];
|
|
605
|
+
const sel = SmoSelector.fromMeasure(this.measures[measureIndex]);
|
|
606
|
+
sel.voice = i;
|
|
607
|
+
sel.tick = voice.notes.length - 1;
|
|
608
|
+
const ties = this.getTiesStartingAt(sel);
|
|
609
|
+
if (ties.length) {
|
|
610
|
+
ties[0].lines.forEach((ll:TieLine ) => {
|
|
611
|
+
rv.push(lastNote.pitches[ll.from]);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return rv;
|
|
616
|
+
}
|
|
584
617
|
getTabStaveForMeasure(selector: SmoSelector): SmoTabStave | undefined {
|
|
585
618
|
return this.tabStaves.find((ts) =>
|
|
586
619
|
SmoSelector.sameStaff(ts.startSelector, selector) && ts.startSelector.measure <= selector.measure
|