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.
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
- SuiDynamicsMenu, SuiTimeSignatureMenu, SuiKeySignatureMenu, SuiStaffModifierMenu,
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
- if (this.score?.isPartExposed() && this.score.staves[0].partInfo.preserveTextGroups) {
112
- mappedStaff = this.score!.staves.find((staff) =>
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
- * Update score based on transposing flag.
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 (!this.isPartExposed()) {
690
+ if (this.isPartExposed()) {
680
691
  const xpose = this.score.preferences?.transposingScore;
681
692
  if (xpose) {
682
- this.score.setTransposing();
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
- // If this is a tranposed score, the displayed score needs to be in 'C'.
731
- // We do this step last since serialize/unserialize work in a pitch transposed
732
- // for the instrument
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
- selector.staff = this.staffMap[selector.staff];
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
- selector, ogText, UndoBuffer.bufferSubtypes.REMOVE);
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
- selector.staff = this.staffMap[0];
153
+ altSelector.staff = this.staffMap[0];
148
154
  } else {
149
- selector.staff = this.staffMap[selector.staff];
155
+ altSelector.staff = this.staffMap[selector.staff];
150
156
  }
151
157
  this.storeUndo.addBuffer('modify text',
152
- bufType, selector, ogtg, UndoBuffer.bufferSubtypes.UPDATE);
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, don't store it in the score text, except for the displayed score
157
- if (!isPartExposed) {
158
- this.storeScore.updateTextGroup(altNew, true);
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.staves[this._getEquivalentStaff(0)].partInfo.updateTextGroup(altNew, true);
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 sel = this.tracker.modifierSelections[0];
312
- if (!sel.selection) {
313
- return PromiseHelpers.emptyPromise();
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.tracker.selections = [sel.selection];
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
- _selectFromToInStaff(score: SmoScore, sel1: SmoSelection, sel2: SmoSelection) {
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 min = SmoSelector.gt(s1.selector, s2.selector) ? s2 : s1;
609
- const max = SmoSelector.lt(min.selector, s2.selector) ? s2 : s1;
610
- this._selectFromToInStaff(score, min, max);
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
- this._selectBetweenSelections(sel1, this.suggestion);
639
- return;
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, printing: boolean, softmax: number) {
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) {
@@ -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 vxMeasure: VxMeasure = new VxMeasure(this.context, selection, printing, softmax);
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();
@@ -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: '6'},
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'},
@@ -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
- current.keySignature = SmoMusic.vexKeySigWithOffset(current.keySignature, -1 * currentInstrument.keyOffset);
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' | 'transposingScore' | 'autoScrollPlayback';
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
- params.measures!.push(measure.serialize());
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
- measure.transposeIndex = instrumentAr[curInstrumentIndex].instrument.keyOffset;
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