Browse Source

merge osmd-public: add cursor.previous() (like next, but backwards), Cursor.SkipInvisibleNotes, various fixes

sschmidTU 2 years ago
parent
commit
b295b554ce

+ 8 - 0
demo/index.html

@@ -53,12 +53,20 @@
                 </div>
             </div>
             <div class="ui vertical buttons">
+                <div class="ui animated fade button" id="previous-cursor-btn">
+                    <div class="visible content">Previous</div>
+                    <div class="hidden content">
+                        <i class="arrow left icon"></i>
+                    </div>
+                </div>
                 <div class="ui animated fade button" id="next-cursor-btn">
                     <div class="visible content">Next</div>
                     <div class="hidden content">
                         <i class="arrow right icon"></i>
                     </div>
                 </div>
+            </div>
+            <div class="ui vertical buttons">
                 <div class="ui animated fade button" id="reset-cursor-btn">
                     <div class="visible content">Reset</div>
                     <div class="hidden content">

+ 10 - 0
demo/index.js

@@ -92,6 +92,7 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
         zoomOuts,
         zoomDivs,
         custom,
+        previousCursorBtn,
         nextCursorBtn,
         resetCursorBtn,
         followCursorCheckbox,
@@ -243,6 +244,7 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
         }
         //canvas.id = 'osmdCanvasDiv';
         //canvas.style.overflowX = 'auto'; // enable horizontal scrolling
+        previousCursorBtn = document.getElementById("previous-cursor-btn");
         nextCursorBtn = document.getElementById("next-cursor-btn");
         resetCursorBtn = document.getElementById("reset-cursor-btn");
         followCursorCheckbox = document.getElementById("follow-cursor-checkbox");
@@ -502,10 +504,18 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
 
         window.addEventListener("keydown", function (e) {
             var event = window.event ? window.event : e;
+            // left arrow key
+            if (event.keyCode === 37) {
+                openSheetMusicDisplay.cursor.previous();
+            }
+            // right arrow key
             if (event.keyCode === 39) {
                 openSheetMusicDisplay.cursor.next();
             }
         });
+        previousCursorBtn.addEventListener("click", function () {
+            openSheetMusicDisplay.cursor.previous();
+        });
         nextCursorBtn.addEventListener("click", function () {
             openSheetMusicDisplay.cursor.next();
         });

+ 4 - 0
src/MusicalScore/Graphical/EngravingRules.ts

@@ -130,7 +130,10 @@ export class EngravingRules {
     public ChordSymbolLabelTexts: Dictionary<ChordSymbolEnum, string>;
     public ChordAccidentalTexts: Dictionary<AccidentalEnum, string>;
     public CustomChords: CustomChord[];
+    /** Not always a symbol, can also be text (RepetitionInstruction). Keeping the name for backwards compatibility. */
     public RepetitionSymbolsYOffset: number;
+    /** Adds a percent of the stave's width (e.g. 0.4 = 40%) to the x position of end instructions like Fine or D.C. al fine */
+    public RepetitionEndInstructionXShiftAsPercentOfStaveWidth: number;
     public RehearsalMarkXOffset: number;
     public RehearsalMarkXOffsetDefault: number;
     public RehearsalMarkXOffsetSystemStartMeasure: number;
@@ -539,6 +542,7 @@ export class EngravingRules {
         this.CustomChords = [];
         this.resetChordNames();
         this.RepetitionSymbolsYOffset = 0;
+        this.RepetitionEndInstructionXShiftAsPercentOfStaveWidth = 0.4; // 40%
         this.RehearsalMarkXOffsetDefault = 10; // avoid collision with metronome number
         this.RehearsalMarkXOffset = 0; // user defined
         this.RehearsalMarkXOffsetSystemStartMeasure = -20; // good test: Haydn Concertante

+ 1 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -452,6 +452,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
         }
         if (instruction) {
             const repetition: VF.Repetition = new VF.Repetition(instruction, xShift, -this.rules.RepetitionSymbolsYOffset);
+            (repetition as any).xShiftAsPercentOfStaveWidth = this.rules.RepetitionEndInstructionXShiftAsPercentOfStaveWidth;
             this.stave.addModifier(repetition, position);
             return;
         }

+ 62 - 0
src/MusicalScore/MusicParts/MusicPartManagerIterator.ts

@@ -87,6 +87,7 @@ export class MusicPartManagerIterator {
     private currentBpm: number;
     private activeDynamicExpressions: AbstractExpression[] = [];
     private activeTempoExpression: MultiTempoExpression;
+    public SkipInvisibleNotes: boolean = true;
 
     public get EndReached(): boolean {
         return this.endReached;
@@ -247,9 +248,33 @@ export class MusicPartManagerIterator {
     public currentPlaybackSettings(): PlaybackSettings {
        return this.musicSheet.SheetPlaybackSetting;
     }
+
+    // move to previous
+    public moveToPrevious(): void {
+        // this.forwardJumpOccurred = this.backJumpOccurred = false;
+        if (this.frontReached) { return; }
+        if (this.currentVoiceEntries) {
+            this.currentVoiceEntries = [];
+        }
+        if (this.currentTimeStamp.RealValue < this.musicSheet.SourceMeasures.length) {
+            this.recursiveMoveBack();
+            return;
+        }
+    }
+
+    public moveToPreviousVisibleVoiceEntry(notesOnly: boolean): void {
+        while (!this.frontReached) {
+            this.moveToPrevious();
+            if (this.checkEntries(notesOnly)) {
+                return;
+            }
+        }
+    }
+
     public moveToNext(): void {
         this.forwardJumpOccurred = this.backJumpOccurred = false;
         if (this.endReached) { return; }
+        if (this.frontReached) { this.frontReached = false; }
         if (this.currentVoiceEntries) {
             this.currentVoiceEntries.length = 0;
         }
@@ -516,6 +541,33 @@ export class MusicPartManagerIterator {
             }
         }
     }
+
+    private recursiveMoveBack(): void {
+       if (this.currentVoiceEntryIndex > 0 ) {
+            this.currentVoiceEntryIndex--;
+            const currentContainer: VerticalSourceStaffEntryContainer = this.currentMeasure.VerticalSourceStaffEntryContainers[this.currentVoiceEntryIndex];
+            this.currentVoiceEntries = this.getVoiceEntries(currentContainer);
+            this.currentVerticalContainerInMeasureTimestamp = currentContainer.Timestamp;
+            this.currentTimeStamp = Fraction.plus(this.currentMeasure.AbsoluteTimestamp, this.currentVerticalContainerInMeasureTimestamp);
+            this.activateCurrentDynamicOrTempoInstructions();
+            return;
+        }
+        else if (this.currentVoiceEntryIndex === 0  && this.currentMeasureIndex !== 0) {
+            const m: SourceMeasure = this.musicSheet.SourceMeasures[this.currentMeasureIndex-1];
+            this.currentMeasureIndex--;
+            this.currentMeasure = this.musicSheet.SourceMeasures[this.currentMeasureIndex];
+            const currentContainer: VerticalSourceStaffEntryContainer = m.VerticalSourceStaffEntryContainers[m.VerticalSourceStaffEntryContainers.length-1];
+            this.currentVoiceEntries = this.getVoiceEntries(currentContainer);
+            this.currentVerticalContainerInMeasureTimestamp = currentContainer.Timestamp;
+            this.currentVoiceEntryIndex = m.VerticalSourceStaffEntryContainers.length-1;
+            this.currentTimeStamp = currentContainer.Timestamp;
+            this.activateCurrentDynamicOrTempoInstructions();
+            return;
+        }
+        // we reached the beginning
+        this.frontReached = true;
+    }
+
     private recursiveMove(): void {
         this.currentVoiceEntryIndex++; // TODO handle hidden part: skip hidden voice if requested by parameter
         if (this.currentVoiceEntryIndex === 0) {
@@ -570,6 +622,16 @@ export class MusicPartManagerIterator {
     }
     private getVisibleEntries(entry: VoiceEntry, visibleEntries: VoiceEntry[]): void {
         if (entry.ParentVoice.Visible) {
+            let anyNoteVisible: boolean = false;
+            for (const note of entry.Notes) {
+                if (note.PrintObject) {
+                    anyNoteVisible = true;
+                    break;
+                }
+            }
+            if (!anyNoteVisible && this.SkipInvisibleNotes) {
+                return;
+            }
             visibleEntries.push(entry);
         }
     }

+ 1 - 1
src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts

@@ -509,7 +509,7 @@ export class ExpressionReader {
                 this.directionTimestamp = Fraction.createFromFraction(inSourceMeasureCurrentFraction);
             }
             const numberXml: number = this.readNumber(dynamicsNode); // probably never given, just to comply with createExpressionIfNeeded()
-            let expressionText: string = dynamicsNode.elements()[0].name;
+            let expressionText: string = dynamicsNode.elements()[0]?.name; // elements can in rare cases still be empty even though hasElements=true, see #1269
             if (expressionText === "other-dynamics") {
                 expressionText = dynamicsNode.elements()[0].value;
             }

+ 7 - 6
src/MusicalScore/ScoreIO/MusicSymbolModules/RepetitionInstructionReader.ts

@@ -143,7 +143,7 @@ export class RepetitionInstructionReader {
         // if (relativeMeasurePosition < 0.5) {
         //   measureIndex--;
         // }
-        const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.DalSegnoAlCoda);
+        const newInstruction: RepetitionInstruction = new RepetitionInstruction(measureIndex, RepetitionInstructionEnum.DaCapoAlCoda);
         this.addInstruction(this.repetitionInstructions, newInstruction);
         return true;
       }
@@ -252,11 +252,12 @@ export class RepetitionInstructionReader {
               instruction.type = RepetitionInstructionEnum.None;
             }
           }
-          if (codaCount === 0 && toCodaCount === 0) {
-            instruction.type = RepetitionInstructionEnum.ToCoda;
-            instruction.alignment = AlignmentType.End;
-            instruction.measureIndex--;
-          }
+          // TODO this prevents a piece consisting of a single coda sign showing coda (will show To Coda)
+          // if (codaCount === 0 && toCodaCount === 0) {
+          //   instruction.type = RepetitionInstructionEnum.ToCoda;
+          //   instruction.alignment = AlignmentType.End;
+          //   instruction.measureIndex--;
+          // }
           break;
         case RepetitionInstructionEnum.Segno:
           if (segnoCount - dalSegnaCount > 0) { // two segnos in a row

+ 20 - 0
src/OpenSheetMusicDisplay/Cursor.ts

@@ -108,6 +108,7 @@ export class Cursor implements IPlaybackListener {
   public hidden: boolean = false;
   public currentPageNumber: number = 1;
   private cursorOptions: CursorOptions;
+  private skipInvisibleNotes: boolean = true;
 
   /** Initialize the cursor. Necessary before using functions like show() and next(). */
   public init(manager: MusicPartManager, graphic: GraphicalMusicSheet): void {
@@ -153,6 +154,8 @@ export class Cursor implements IPlaybackListener {
     }
 
     this.iterator = this.manager.getIterator();
+    // remember SkipInvisibleNotes setting, which otherwise gets reset to default value
+    this.iterator.SkipInvisibleNotes = this.skipInvisibleNotes;
   }
 
   private getStaffEntryFromVoiceEntry(voiceEntry: VoiceEntry): VexFlowStaffEntry {
@@ -341,6 +344,14 @@ export class Cursor implements IPlaybackListener {
   }
 
   /**
+   * Go to previous entry
+   */
+   public previous(): void {
+    this.iterator.moveToPreviousVisibleVoiceEntry(false);
+    this.update();
+  }
+
+  /**
    * Go to next entry
    */
   public next(): void {
@@ -451,6 +462,15 @@ export class Cursor implements IPlaybackListener {
     return 1;
   }
 
+  public get SkipInvisibleNotes(): boolean {
+    return this.skipInvisibleNotes;
+  }
+
+  public set SkipInvisibleNotes(value: boolean) {
+    this.skipInvisibleNotes = value;
+    this.iterator.SkipInvisibleNotes = value;
+  }
+
   public updateCurrentPageFromSystem(system: MusicSystem): number {
     if (system?.Parent) {
       const newPageNumber: number = system.Parent.PageNumber;

+ 25 - 1
src/VexFlowPatch/src/staverepetition.js

@@ -117,7 +117,8 @@ export class Repetition extends StaveModifier {
     let symbol_x = x + this.x_shift;
     if (this.symbol_type === Repetition.type.CODA_LEFT) {
       // Offset Coda text to right of stave beginning
-      text_x = this.x + stave.options.vertical_bar_width;
+      // text_x = this.x + stave.options.vertical_bar_width;
+      text_x = this.x + this.x_shift;
       symbol_x = text_x + ctx.measureText(text).width + 12;
     } else {
       // VexFlowPatch: fix placement, like for DS_AL_CODA
@@ -126,6 +127,29 @@ export class Repetition extends StaveModifier {
       text_x = this.x + this.x_shift + stave.options.vertical_bar_width;
       symbol_x = text_x + ctx.measureText(text).width + 12;
     }
+    if (this.xShiftAsPercentOfStaveWidth) {
+      const extraShiftX = stave.width * this.xShiftAsPercentOfStaveWidth;
+      if (
+        this.symbol_type === Repetition.type.DC_AL_FINE ||
+        this.symbol_type === Repetition.type.FINE ||
+        this.symbol_type === Repetition.type.DC ||
+        this.symbol_type === Repetition.type.DS_AL_FINE ||
+        this.symbol_type === Repetition.type.DS ||
+        this.symbol_type === Repetition.type.FINE
+      ) {
+        text_x += extraShiftX;
+      }
+      // else if (
+      //   this.symbol_type === Repetition.type.DS_AL_CODA ||
+      //   this.symbol_type === Repetition.type.DC_AL_CODA ||
+      //   this.symbol_type === Repetition.type.TO_CODA
+      // ) {
+      //   // somehow DS_AL_CODA is already further right by default
+      //   //   TODO can cause collisions
+      //   text_x += extraShiftX * 0.4;
+      //   symbol_x += extraShiftX * 0.4;
+      // }
+    }
     // earlier, we applied this to most elements individually, not necessary:
     // } else if (this.symbol_type === Repetition.type.TO_CODA) {
     //   // text_x = x + this.x + this.x_shift + stave.options.vertical_bar_width;