Explorar el Código

feat(MultipleRest): Render multiple measure rests. add option to not render them. (#506)

close #506

final barline doesn't work yet if a multiple rest ends the piece.

VexFlowMultiRestMeasure only extends GraphicalMeasure for now, but can also extend VexFlowMeasure if desired

had to add a couple null checks for GraphicalMeasures, because there can now be less graphical measures than
source measures in the piece. (a multiple rest measure means the rest measures after the first will not be rendered)
sschmid hace 5 años
padre
commit
7a04814e7f

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

@@ -167,6 +167,7 @@ export class EngravingRules {
     private systemBoldLineWidth: number;
     private systemRepetitionEndingLineWidth: number;
     private systemDotWidth: number;
+    private multipleRestMeasureDefaultWidth: number;
     private distanceBetweenVerticalSystemLines: number;
     private distanceBetweenDotAndLine: number;
     private octaveShiftLineWidth: number;
@@ -219,6 +220,7 @@ export class EngravingRules {
     private renderFingerings: boolean;
     private renderMeasureNumbers: boolean;
     private renderLyrics: boolean;
+    private renderMultipleRestMeasures: boolean;
     private renderTimeSignatures: boolean;
     private dynamicExpressionMaxDistance: number;
     private dynamicExpressionSpacer: number;
@@ -425,6 +427,8 @@ export class EngravingRules {
         this.octaveShiftVerticalLineLength = EngravingRules.unit;
         this.graceLineWidth = this.staffLineWidth * this.GraceNoteScalingFactor;
 
+        this.multipleRestMeasureDefaultWidth = 4;
+
         // Line Widths
         this.minimumCrossedBeamDifferenceMargin = 0.0001;
 
@@ -467,6 +471,7 @@ export class EngravingRules {
         this.renderFingerings = true;
         this.renderMeasureNumbers = true;
         this.renderLyrics = true;
+        this.renderMultipleRestMeasures = true;
         this.renderTimeSignatures = true;
         this.fingeringPosition = PlacementEnum.Left; // easier to get bounding box, and safer for vertical layout
         this.fingeringInsideStafflines = false;
@@ -1345,6 +1350,12 @@ export class EngravingRules {
     public set SystemDotWidth(value: number) {
         this.systemDotWidth = value;
     }
+    public get MultipleRestMeasureDefaultWidth(): number {
+        return this.multipleRestMeasureDefaultWidth;
+    }
+    public set MultipleRestMeasureDefaultWidth(value: number) {
+        this.multipleRestMeasureDefaultWidth = value;
+    }
     public get DistanceBetweenVerticalSystemLines(): number {
         return this.distanceBetweenVerticalSystemLines;
     }
@@ -1643,6 +1654,12 @@ export class EngravingRules {
     public set RenderLyrics(value: boolean) {
         this.renderLyrics = value;
     }
+    public get RenderMultipleRestMeasures(): boolean {
+        return this.renderMultipleRestMeasures;
+    }
+    public set RenderMultipleRestMeasures(value: boolean) {
+        this.renderMultipleRestMeasures = value;
+    }
     public get RenderTimeSignatures(): boolean {
         return this.renderTimeSignatures;
     }

+ 3 - 0
src/MusicalScore/Graphical/GraphicalMusicSheet.ts

@@ -213,6 +213,9 @@ export class GraphicalMusicSheet {
     public findGraphicalStaffEntryFromMeasureList(staffIndex: number, measureIndex: number, sourceStaffEntry: SourceStaffEntry): GraphicalStaffEntry {
         for (let i: number = measureIndex; i < this.measureList.length; i++) {
             const graphicalMeasure: GraphicalMeasure = this.measureList[i][staffIndex];
+            if (!graphicalMeasure) {
+                continue;
+            }
             for (let idx: number = 0, len: number = graphicalMeasure.staffEntries.length; idx < len; ++idx) {
                 const graphicalStaffEntry: GraphicalStaffEntry = graphicalMeasure.staffEntries[idx];
                 if (graphicalStaffEntry.sourceStaffEntry === sourceStaffEntry) {

+ 32 - 2
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -105,7 +105,9 @@ export abstract class MusicSheetCalculator {
     protected static setMeasuresMinStaffEntriesWidth(measures: GraphicalMeasure[], minimumStaffEntriesWidth: number): void {
         for (let idx: number = 0, len: number = measures.length; idx < len; ++idx) {
             const measure: GraphicalMeasure = measures[idx];
-            measure.minimumStaffEntriesWidth = minimumStaffEntriesWidth;
+            if (measure) {
+                measure.minimumStaffEntriesWidth = minimumStaffEntriesWidth;
+            }
         }
     }
 
@@ -165,6 +167,16 @@ export abstract class MusicSheetCalculator {
                 activeClefs
             );
             measureList.push(graphicalMeasures);
+            if (sourceMeasure.multipleRestMeasures && this.rules.RenderMultipleRestMeasures) {
+                const measuresToSkip: number = sourceMeasure.multipleRestMeasures - 1;
+                // console.log(`skipping ${measuresToSkip} measures for measure #${sourceMeasure.MeasureNumber}.`);
+                idx += measuresToSkip;
+                for (let idx2: number = 0; idx2 < measuresToSkip; idx2++) {
+                    measureList.push([undefined]);
+                    // TODO we could push an object here or push nothing entirely,
+                    //   but then the index doesn't correspond to measure numbers anymore.
+                }
+            }
         }
 
         const staffIsPercussionArray: Array<boolean> =
@@ -659,7 +671,7 @@ export abstract class MusicSheetCalculator {
             for (let idx2: number = 0, len2: number = graphicalMeasures.length; idx2 < len2; ++idx2) {
                 const graphicalMeasure: GraphicalMeasure = allMeasures[idx][idx2];
 
-                if (graphicalMeasure.isVisible()) {
+                if (graphicalMeasure?.isVisible()) {
                     visiblegraphicalMeasures.push(graphicalMeasure);
 
                     if (this.rules.ColoringEnabled) {
@@ -1976,6 +1988,9 @@ export abstract class MusicSheetCalculator {
         for (let i: number = 0; i < this.graphicalMusicSheet.MeasureList.length; i++) {
             for (let j: number = 0; j < numberOfEntries; j++) {
                 const measure: GraphicalMeasure = this.graphicalMusicSheet.MeasureList[i][j];
+                if (!measure) {
+                    continue;
+                }
                 for (let idx: number = 0, len: number = measure.staffEntries.length; idx < len; ++idx) {
                     const graphicalStaffEntry: GraphicalStaffEntry = measure.staffEntries[idx];
                     const verticalContainer: VerticalGraphicalStaffEntryContainer =
@@ -2026,6 +2041,8 @@ export abstract class MusicSheetCalculator {
         if (activeClefs[staffIndex].ClefType === ClefEnum.TAB) {
             staff.isTab = true;
             measure = MusicSheetCalculator.symbolFactory.createTabStaffMeasure(sourceMeasure, staff);
+        } else if (sourceMeasure.multipleRestMeasures && this.rules.RenderMultipleRestMeasures) {
+            measure = MusicSheetCalculator.symbolFactory.createMultiRestMeasure(sourceMeasure, staff);
         } else {
             measure = MusicSheetCalculator.symbolFactory.createGraphicalMeasure(sourceMeasure, staff);
         }
@@ -2214,6 +2231,9 @@ export abstract class MusicSheetCalculator {
             const measures: GraphicalMeasure[] = this.graphicalMusicSheet.MeasureList[idx];
             for (let idx2: number = 0, len2: number = measures.length; idx2 < len2; ++idx2) {
                 const measure: GraphicalMeasure = measures[idx2];
+                if (!measure) {
+                    continue;
+                }
                 //This property is active...
                 if (this.rules.PercussionOneLineCutoff !== undefined && this.rules.PercussionOneLineCutoff !== 0) {
                     //We have a percussion clef, check to see if this property applies...
@@ -2714,6 +2734,10 @@ export abstract class MusicSheetCalculator {
         for (let i: number = minIndex; i <= maxIndex; i++) {
             const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
             for (let j: number = 0; j < sourceMeasure.StaffLinkedExpressions.length; j++) {
+                if (!this.graphicalMusicSheet.MeasureList[i] || !this.graphicalMusicSheet.MeasureList[i][j]) {
+                    continue;
+                }
+
                 if (this.graphicalMusicSheet.MeasureList[i][j].ParentStaff.ParentInstrument.Visible) {
                     for (let k: number = 0; k < sourceMeasure.StaffLinkedExpressions[j].length; k++) {
                         if (sourceMeasure.StaffLinkedExpressions[j][k].InstantaneousDynamic !== undefined ||
@@ -2733,6 +2757,9 @@ export abstract class MusicSheetCalculator {
         for (let i: number = 0; i < this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length; i++) {
             const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
             for (let j: number = 0; j < sourceMeasure.StaffLinkedExpressions.length; j++) {
+                if (!this.graphicalMusicSheet.MeasureList[i] || !this.graphicalMusicSheet.MeasureList[i][j]) {
+                    continue;
+                }
                 if (this.graphicalMusicSheet.MeasureList[i][j].ParentStaff.ParentInstrument.Visible) {
                     for (let k: number = 0; k < sourceMeasure.StaffLinkedExpressions[j].length; k++) {
                         if ((sourceMeasure.StaffLinkedExpressions[j][k].OctaveShiftStart)) {
@@ -2805,6 +2832,9 @@ export abstract class MusicSheetCalculator {
         for (let i: number = 0; i < this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length; i++) {
             const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
             for (let j: number = 0; j < sourceMeasure.StaffLinkedExpressions.length; j++) {
+                if (!this.graphicalMusicSheet.MeasureList[i] || !this.graphicalMusicSheet.MeasureList[i][j]) {
+                    continue;
+                }
                 if (this.graphicalMusicSheet.MeasureList[i][j].ParentStaff.ParentInstrument.Visible) {
                     for (let k: number = 0; k < sourceMeasure.StaffLinkedExpressions[j].length; k++) {
                         if ((sourceMeasure.StaffLinkedExpressions[j][k].MoodList.length > 0) ||

+ 20 - 11
src/MusicalScore/Graphical/MusicSystemBuilder.ts

@@ -62,16 +62,21 @@ export class MusicSystemBuilder {
         // the first System - create also its Labels
         this.currentSystemParams.currentSystem = this.initMusicSystem();
 
-        let numberOfMeasures: number = 0;
-        for (let idx: number = 0, len: number = this.measureList.length; idx < len; ++idx) {
-            if (this.measureList[idx].length > 0) {
-                numberOfMeasures++;
-            }
-        }
+        // let numberOfMeasures: number = 0;
+        // for (let idx: number = 0, len: number = this.measureList.length; idx < len; ++idx) {
+        //     if (this.measureList[idx].length > 0) {
+        //         numberOfMeasures++;
+        //     }
+        // }
+        // console.log(`numberOfMeasures: ${numberOfMeasures}`);
 
         // go through measures and add to system until system gets too long -> finish system and start next system [line break, new system].
-        while (this.measureListIndex < numberOfMeasures) {
+        while (this.measureListIndex < this.measureList.length) {
             const graphicalMeasures: GraphicalMeasure[] = this.measureList[this.measureListIndex];
+            if (!graphicalMeasures || !graphicalMeasures[0]) {
+                this.measureListIndex++;
+                continue; // previous measure was probably multi-rest, skip this one
+            }
             for (let idx: number = 0, len: number = graphicalMeasures.length; idx < len; ++idx) {
                 graphicalMeasures[idx].resetLayout();
             }
@@ -114,12 +119,16 @@ export class MusicSystemBuilder {
             let nextSourceMeasure: SourceMeasure = undefined;
             if (this.measureListIndex + 1 < this.measureList.length) {
                 const nextGraphicalMeasures: GraphicalMeasure[] = this.measureList[this.measureListIndex + 1];
-                nextSourceMeasure = nextGraphicalMeasures[0].parentSourceMeasure;
-                if (nextSourceMeasure.hasBeginInstructions()) {
+                // TODO: consider multirest. then the next graphical measure may not exist. but there shouldn't be hidden changes here.
+                nextSourceMeasure = nextGraphicalMeasures[0]?.parentSourceMeasure;
+                if (nextSourceMeasure?.hasBeginInstructions()) {
                     nextMeasureBeginInstructionWidth += this.addBeginInstructions(nextGraphicalMeasures, false, false);
                 }
             }
-            const totalMeasureWidth: number = currentMeasureBeginInstructionsWidth + currentMeasureEndInstructionsWidth + currentMeasureVarWidth;
+            let totalMeasureWidth: number = currentMeasureBeginInstructionsWidth + currentMeasureEndInstructionsWidth + currentMeasureVarWidth;
+            if (graphicalMeasures[0]?.parentSourceMeasure?.multipleRestMeasures) {
+                totalMeasureWidth = this.rules.MultipleRestMeasureDefaultWidth; // default 4 (12 seems too large)
+            }
             const measureFitsInSystem: boolean = this.currentSystemParams.currentWidth + totalMeasureWidth + nextMeasureBeginInstructionWidth < systemMaxWidth;
             const doXmlPageBreak: boolean = this.rules.NewPageAtXMLNewPageAttribute && sourceMeasure.printNewPageXml;
             const doXmlLineBreak: boolean = doXmlPageBreak || // also create new system if doing page break
@@ -816,7 +825,7 @@ export class MusicSystemBuilder {
     protected getNextMeasureKeyInstruction(): KeyInstruction {
         if (this.measureListIndex < this.measureList.length - 1) {
             for (let visIndex: number = 0; visIndex < this.measureList[this.measureListIndex].length; visIndex++) {
-                const sourceMeasure: SourceMeasure = this.measureList[this.measureListIndex + 1][visIndex].parentSourceMeasure;
+                const sourceMeasure: SourceMeasure = this.measureList[this.measureListIndex + 1][visIndex]?.parentSourceMeasure;
                 if (!sourceMeasure) {
                     return undefined;
                 }

+ 1 - 1
src/MusicalScore/Graphical/SkyBottomLineCalculator.ts

@@ -64,7 +64,7 @@ export class SkyBottomLineCalculator {
             const oldMeasureWidth: number = vsStaff.getWidth();
             // We need to tell the VexFlow stave about the canvas width. This looks
             // redundant because it should know the canvas but somehow it doesn't.
-            // Maybe I am overlooking something but for no this does the trick
+            // Maybe I am overlooking something but for now this does the trick
             vsStaff.setWidth(width);
             measure.format();
             vsStaff.setWidth(oldMeasureWidth);

+ 11 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalSymbolFactory.ts

@@ -28,6 +28,7 @@ import { VexFlowConverter } from "./VexFlowConverter";
 import { VexFlowTabMeasure } from "./VexFlowTabMeasure";
 import { VexFlowStaffLine } from "./VexFlowStaffLine";
 import { KeyInstruction } from "../../VoiceData/Instructions/KeyInstruction";
+import { VexFlowMultiRestMeasure } from "./VexFlowMultiRestMeasure";
 
 export class VexFlowGraphicalSymbolFactory implements IGraphicalSymbolFactory {
     /**
@@ -62,6 +63,16 @@ export class VexFlowGraphicalSymbolFactory implements IGraphicalSymbolFactory {
     }
 
     /**
+     * Construct a MultiRestMeasure from the given source measure and staff.
+     * @param sourceMeasure
+     * @param staff
+     * @returns {VexFlowMultiRestMeasure}
+     */
+    public createMultiRestMeasure(sourceMeasure: SourceMeasure, staff: Staff, staffLine?: StaffLine): GraphicalMeasure {
+        return new VexFlowMultiRestMeasure(staff, sourceMeasure, staffLine);
+    }
+
+    /**
      * Construct an empty Tab staffMeasure from the given source measure and staff.
      * @param sourceMeasure
      * @param staff

+ 617 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowMultiRestMeasure.ts

@@ -0,0 +1,617 @@
+import Vex from "vexflow";
+import {GraphicalMeasure} from "../GraphicalMeasure";
+import {SourceMeasure} from "../../VoiceData/SourceMeasure";
+import {Staff} from "../../VoiceData/Staff";
+import {StaffLine} from "../StaffLine";
+import {SystemLinesEnum} from "../SystemLinesEnum";
+import {ClefInstruction, ClefEnum} from "../../VoiceData/Instructions/ClefInstruction";
+import {KeyInstruction} from "../../VoiceData/Instructions/KeyInstruction";
+import {RhythmInstruction} from "../../VoiceData/Instructions/RhythmInstruction";
+import {VexFlowConverter} from "./VexFlowConverter";
+import {Beam} from "../../VoiceData/Beam";
+import {GraphicalNote} from "../GraphicalNote";
+import {GraphicalStaffEntry} from "../GraphicalStaffEntry";
+import StaveConnector = Vex.Flow.StaveConnector;
+import {unitInPixels} from "./VexFlowMusicSheetDrawer";
+import {Tuplet} from "../../VoiceData/Tuplet";
+import {RepetitionInstructionEnum, RepetitionInstruction, AlignmentType} from "../../VoiceData/Instructions/RepetitionInstruction";
+import {SystemLinePosition} from "../SystemLinePosition";
+import {GraphicalVoiceEntry} from "../GraphicalVoiceEntry";
+import {Voice} from "../../VoiceData/Voice";
+import {EngravingRules} from "../EngravingRules";
+import {SkyBottomLineCalculator} from "../SkyBottomLineCalculator";
+import {VexFlowMeasure} from "./VexFlowMeasure";
+
+// type StemmableNote = Vex.Flow.StemmableNote;
+
+/** A GraphicalMeasure drawing a multiple-rest measure in Vexflow.
+ *  Mostly copied from VexFlowMeasure.
+ *  Even though most of those functions aren't needed, apparently you can't remove the layoutStaffEntry function.
+ */
+export class VexFlowMultiRestMeasure extends GraphicalMeasure {
+    private multiRestElement: any; // VexFlow: Element
+
+    constructor(staff: Staff, sourceMeasure: SourceMeasure = undefined, staffLine: StaffLine = undefined) {
+        super(staff, sourceMeasure, staffLine);
+        this.minimumStaffEntriesWidth = -1;
+
+        /*
+         * There is no case in which `staffLine === undefined && sourceMeasure === undefined` holds.
+         * Hence, it is not necessary to specify an `else` case.
+         * One can verify this through a usage search for this constructor.
+         */
+        if (staffLine) {
+            this.rules = staffLine.ParentMusicSystem.rules;
+        } else if (sourceMeasure) {
+            this.rules = sourceMeasure.Rules;
+        }
+
+        this.resetLayout();
+
+        // type note: Vex.Flow.MultiMeasureRest is not in the DefinitelyTyped definitions yet.
+        this.multiRestElement = new (Vex.Flow as any).MultiMeasureRest(sourceMeasure.multipleRestMeasures, {
+            // number_line: 3
+        });
+    }
+
+    /** The VexFlow Stave (= one measure in a staffline) */
+    protected stave: Vex.Flow.Stave;
+    /** VexFlow StaveConnectors (vertical lines) */
+    protected connectors: Vex.Flow.StaveConnector[] = [];
+    // The engraving rules of OSMD.
+    public rules: EngravingRules;
+
+    // Sets the absolute coordinates of the VFStave on the canvas
+    public setAbsoluteCoordinates(x: number, y: number): void {
+        this.stave.setX(x).setY(y);
+    }
+
+    /**
+     * Reset all the geometric values and parameters of this measure and put it in an initialized state.
+     * This is needed to evaluate a measure a second time by system builder.
+     */
+    public resetLayout(): void {
+        // Take into account some space for the begin and end lines of the stave
+        // Will be changed when repetitions will be implemented
+        //this.beginInstructionsWidth = 20 / UnitInPixels;
+        //this.endInstructionsWidth = 20 / UnitInPixels;
+
+        // TODO save beginning and end bar type, set these again after new stave.
+
+        this.stave = new Vex.Flow.Stave(0, 0, 0, {
+            space_above_staff_ln: 0,
+            space_below_staff_ln: 0,
+        });
+
+        if (this.ParentStaff) {
+            this.setLineNumber(this.ParentStaff.StafflineCount);
+        }
+        // constructor sets beginning and end bar type to standard
+
+        this.stave.setBegBarType(Vex.Flow.Barline.type.NONE); // technically not correct, but we'd need to set the next measure's beginning bar type
+        if (this.parentSourceMeasure && this.parentSourceMeasure.endingBarStyleEnum === SystemLinesEnum.None) {
+            // fix for vexflow ignoring ending barline style after new stave, apparently
+            this.stave.setEndBarType(Vex.Flow.Barline.type.NONE);
+        }
+        // the correct bar types seem to be set later
+
+        this.updateInstructionWidth();
+    }
+
+    public clean(): void {
+        this.connectors = [];
+        // Clean up instructions
+        this.resetLayout();
+    }
+
+    /**
+     * returns the x-width (in units) of a given measure line {SystemLinesEnum}.
+     * @param line
+     * @returns the x-width in osmd units
+     */
+    public getLineWidth(line: SystemLinesEnum): number {
+        switch (line) {
+            // return 0 for the normal lines, as the line width will be considered at the updateInstructionWidth() method using the stavemodifiers.
+            // case SystemLinesEnum.SingleThin:
+            //     return 5.0 / unitInPixels;
+            // case SystemLinesEnum.DoubleThin:
+            //     return 5.0 / unitInPixels;
+            //     case SystemLinesEnum.ThinBold:
+            //     return 5.0 / unitInPixels;
+            // but just add a little extra space for repetitions (cosmetics):
+            case SystemLinesEnum.BoldThinDots:
+            case SystemLinesEnum.DotsThinBold:
+                return 10.0 / unitInPixels;
+            case SystemLinesEnum.DotsBoldBoldDots:
+                return 10.0 / unitInPixels;
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * adds the given clef to the begin of the measure.
+     * This has to update/increase BeginInstructionsWidth.
+     * @param clef
+     */
+    public addClefAtBegin(clef: ClefInstruction): void {
+        if (clef.ClefType === ClefEnum.TAB) {
+            this.stave.addClef("tab", undefined, undefined, undefined);
+        } else {
+        const vfclef: { type: string, size: string, annotation: string } = VexFlowConverter.Clef(clef, "default");
+        this.stave.addClef(vfclef.type, vfclef.size, vfclef.annotation, Vex.Flow.StaveModifier.Position.BEGIN);
+        }
+        this.updateInstructionWidth();
+    }
+
+    /**
+     * Sets the number of stafflines that are rendered, so that they are centered properly
+     * @param lineNumber
+     */
+    public setLineNumber(lineNumber: number): void {
+        if (lineNumber !== 5) {
+            if (lineNumber === 0) {
+                (this.stave as any).setNumLines(0);
+                this.stave.getBottomLineY = function(): number {
+                    return this.getYForLine(this.options.num_lines);
+                };
+            } else if (lineNumber === 1) {
+                // Vex.Flow.Stave.setNumLines hides all but the top line.
+                // this is better
+                (this.stave.options as any).line_config = [
+                    { visible: false },
+                    { visible: false },
+                    { visible: true }, // show middle
+                    { visible: false },
+                    { visible: false },
+                ];
+                //quick fix to see if this matters for calculation. Doesn't seem to
+                this.stave.getBottomLineY = function(): number {
+                    return this.getYForLine(2);
+                };
+                //lines (which isn't this case here)
+                //this.stave.options.num_lines = parseInt(lines, 10);
+            } else if (lineNumber === 2) {
+                (this.stave.options as any).line_config = [
+                    { visible: false },
+                    { visible: false },
+                    { visible: true }, // show middle
+                    { visible: true },
+                    { visible: false },
+                ];
+                this.stave.getBottomLineY = function(): number {
+                    return this.getYForLine(3);
+                };
+            } else if (lineNumber === 3) {
+                (this.stave.options as any).line_config = [
+                    { visible: false },
+                    { visible: true },
+                    { visible: true }, // show middle
+                    { visible: true },
+                    { visible: false },
+                ];
+                this.stave.getBottomLineY = function(): number {
+                    return this.getYForLine(2);
+                };
+            } else {
+                (this.stave as any).setNumLines(lineNumber);
+                this.stave.getBottomLineY = function(): number {
+                    return this.getYForLine(this.options.num_lines);
+                };
+            }
+        }
+    }
+
+    /**
+     * adds the given key to the begin of the measure.
+     * This has to update/increase BeginInstructionsWidth.
+     * @param currentKey the new valid key.
+     * @param previousKey the old cancelled key. Needed to show which accidentals are not valid any more.
+     * @param currentClef the valid clef. Needed to put the accidentals on the right y-positions.
+     */
+    public addKeyAtBegin(currentKey: KeyInstruction, previousKey: KeyInstruction, currentClef: ClefInstruction): void {
+        // this.stave.setKeySignature(
+        //     VexFlowConverter.keySignature(currentKey),
+        //     VexFlowConverter.keySignature(previousKey),
+        //     undefined
+        // );
+        // this.updateInstructionWidth();
+    }
+
+    /**
+     * adds the given rhythm to the begin of the measure.
+     * This has to update/increase BeginInstructionsWidth.
+     * @param rhythm
+     */
+    public addRhythmAtBegin(rhythm: RhythmInstruction): void {
+        // const timeSig: Vex.Flow.TimeSignature = VexFlowConverter.TimeSignature(rhythm);
+        // this.stave.addModifier(
+        //     timeSig,
+        //     Vex.Flow.StaveModifier.Position.BEGIN
+        // );
+        // this.updateInstructionWidth();
+    }
+
+    /**
+     * adds the given clef to the end of the measure.
+     * This has to update/increase EndInstructionsWidth.
+     * @param clef
+     */
+    public addClefAtEnd(clef: ClefInstruction): void {
+        // const vfclef: { type: string, size: string, annotation: string } = VexFlowConverter.Clef(clef, "small");
+        // this.stave.setEndClef(vfclef.type, vfclef.size, vfclef.annotation);
+        // this.updateInstructionWidth();
+    }
+
+    public addMeasureLine(lineType: SystemLinesEnum, linePosition: SystemLinePosition): void {
+        switch (linePosition) {
+            case SystemLinePosition.MeasureBegin:
+                switch (lineType) {
+                    case SystemLinesEnum.BoldThinDots:
+                        this.stave.setBegBarType(Vex.Flow.Barline.type.REPEAT_BEGIN);
+                        break;
+                    default:
+                        //this.stave.setBegBarType(Vex.Flow.Barline.type.NONE); // not necessary, it seems
+                        break;
+                }
+                break;
+            case SystemLinePosition.MeasureEnd:
+                switch (lineType) {
+                    case SystemLinesEnum.DotsBoldBoldDots:
+                        this.stave.setEndBarType(Vex.Flow.Barline.type.REPEAT_BOTH);
+                        break;
+                    case SystemLinesEnum.DotsThinBold:
+                        this.stave.setEndBarType(Vex.Flow.Barline.type.REPEAT_END);
+                        break;
+                    case SystemLinesEnum.DoubleThin:
+                        this.stave.setEndBarType(Vex.Flow.Barline.type.DOUBLE);
+                        break;
+                    case SystemLinesEnum.ThinBold:
+                        this.stave.setEndBarType(Vex.Flow.Barline.type.END);
+                        break;
+                    case SystemLinesEnum.None:
+                        this.stave.setEndBarType(Vex.Flow.Barline.type.NONE);
+                        break;
+                    // TODO: Add support for additional Barline types when VexFlow supports them
+                    default:
+                        break;
+                }
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Adds a measure number to the top left corner of the measure
+     * This method is not used currently in favor of the calculateMeasureNumberPlacement
+     * method in the MusicSheetCalculator.ts
+     */
+    public addMeasureNumber(): void {
+        const text: string = this.MeasureNumber.toString();
+        const position: number = StavePositionEnum.ABOVE;  //Vex.Flow.StaveModifier.Position.ABOVE;
+        const options: any = {
+            justification: 1,
+            shift_x: 0,
+            shift_y: 0,
+          };
+
+        this.stave.setText(text, position, options);
+    }
+
+    public addWordRepetition(repetitionInstruction: RepetitionInstruction): void {
+        let instruction: Vex.Flow.Repetition.type = undefined;
+        let position: any = Vex.Flow.StaveModifier.Position.END;
+        switch (repetitionInstruction.type) {
+          case RepetitionInstructionEnum.Segno:
+            // create Segno Symbol:
+            instruction = Vex.Flow.Repetition.type.SEGNO_LEFT;
+            position = Vex.Flow.StaveModifier.Position.BEGIN;
+            break;
+          case RepetitionInstructionEnum.Coda:
+            // create Coda Symbol:
+            instruction = Vex.Flow.Repetition.type.CODA_LEFT;
+            position = Vex.Flow.StaveModifier.Position.BEGIN;
+            break;
+          case RepetitionInstructionEnum.DaCapo:
+            instruction = Vex.Flow.Repetition.type.DC;
+            break;
+          case RepetitionInstructionEnum.DalSegno:
+            instruction = Vex.Flow.Repetition.type.DS;
+            break;
+          case RepetitionInstructionEnum.Fine:
+            instruction = Vex.Flow.Repetition.type.FINE;
+            break;
+          case RepetitionInstructionEnum.ToCoda:
+            //instruction = "To Coda";
+            break;
+          case RepetitionInstructionEnum.DaCapoAlFine:
+            instruction = Vex.Flow.Repetition.type.DC_AL_FINE;
+            break;
+          case RepetitionInstructionEnum.DaCapoAlCoda:
+            instruction = Vex.Flow.Repetition.type.DC_AL_CODA;
+            break;
+          case RepetitionInstructionEnum.DalSegnoAlFine:
+            instruction = Vex.Flow.Repetition.type.DS_AL_FINE;
+            break;
+          case RepetitionInstructionEnum.DalSegnoAlCoda:
+            instruction = Vex.Flow.Repetition.type.DS_AL_CODA;
+            break;
+          default:
+            break;
+        }
+        if (instruction) {
+            this.stave.addModifier(new Vex.Flow.Repetition(instruction, 0, 0), position);
+            return;
+        }
+
+        this.addVolta(repetitionInstruction);
+    }
+
+    private addVolta(repetitionInstruction: RepetitionInstruction): void {
+        let voltaType: number = Vex.Flow.Volta.type.BEGIN;
+        if (repetitionInstruction.type === RepetitionInstructionEnum.Ending) {
+            switch (repetitionInstruction.alignment) {
+                case AlignmentType.Begin:
+                    if (this.parentSourceMeasure.endsRepetitionEnding()) {
+                        voltaType = Vex.Flow.Volta.type.BEGIN_END;
+                    } else {
+                        voltaType = Vex.Flow.Volta.type.BEGIN;
+                    }
+                    break;
+                case AlignmentType.End:
+                    if (this.parentSourceMeasure.beginsRepetitionEnding()) {
+                        //voltaType = Vex.Flow.Volta.type.BEGIN_END;
+                        // don't add BEGIN_END volta a second time:
+                        return;
+                    } else {
+                        voltaType = Vex.Flow.Volta.type.END;
+                    }
+                    break;
+                default:
+                    break;
+            }
+
+            const skyBottomLineCalculator: SkyBottomLineCalculator = this.ParentStaffLine.SkyBottomLineCalculator;
+            //Because of loss of accuracy when sampling (see SkyBottomLineCalculator.updateInRange), measures tend to overlap
+            //This causes getSkyLineMinInRange to return an incorrect min value (one from the previous measure, which has been modified)
+            //We need to offset the end of what we are measuring by a bit to prevent this, otherwise volta pairs step up
+            const start: number = this.PositionAndShape.AbsolutePosition.x + this.PositionAndShape.BorderMarginLeft + 0.4;
+            const end: number = this.PositionAndShape.AbsolutePosition.x + this.PositionAndShape.BorderMarginRight;
+            //2 unit gap, since volta is positioned from y center it seems.
+            //This prevents cases where the volta is rendered over another element
+            const skylineMinForMeasure: number = skyBottomLineCalculator.getSkyLineMinInRange( start, end ) - 2;
+            //-6 OSMD units is the 0 value that the volta is placed on. .1 extra so the skyline goes above the volta
+            //instead of on the line itself
+            let newSkylineValueForMeasure: number = -6.1 + this.rules.VoltaOffset;
+            let vexFlowVoltaHeight: number = this.rules.VoltaOffset;
+            //EngravingRules default offset is 2.5, can be user set.
+            //2.5 gives us a good default value to work with.
+
+            //if we calculate that the minimum skyline allowed by elements is above the default volta position, need to adjust volta up further
+            if (skylineMinForMeasure < newSkylineValueForMeasure) {
+                const skylineDifference: number = skylineMinForMeasure - newSkylineValueForMeasure;
+                vexFlowVoltaHeight += skylineDifference;
+                newSkylineValueForMeasure = skylineMinForMeasure;
+            }
+
+            let prevMeasure: VexFlowMeasure = undefined;
+            //if we already have a volta in the prev measure, should match it's height, or if we are higher, it should match ours
+            //find previous sibling measure that may have volta
+            const currentMeasureNumber: number = this.parentSourceMeasure.MeasureNumber;
+            for (let i: number = 0; i < this.ParentStaffLine.Measures.length; i++) {
+                const tempMeasure: GraphicalMeasure = this.ParentStaffLine.Measures[i];
+                if (!(tempMeasure instanceof VexFlowMeasure)) {
+                    //should never be the case... But check just to be sure
+                    continue;
+                }
+                if (tempMeasure.MeasureNumber === currentMeasureNumber - 1) {
+                    //We found the previous top measure
+                    prevMeasure = tempMeasure as VexFlowMeasure;
+                }
+            }
+
+            if (prevMeasure) {
+                const prevStaveModifiers: Vex.Flow.StaveModifier[] = (prevMeasure as any).stave?.getModifiers();
+                for (let i: number = 0; i < prevStaveModifiers.length; i++) {
+                    const nextStaveModifier: Vex.Flow.StaveModifier = prevStaveModifiers[i];
+                    if (nextStaveModifier.hasOwnProperty("volta")) {
+                        const prevskyBottomLineCalculator: SkyBottomLineCalculator = prevMeasure.ParentStaffLine.SkyBottomLineCalculator;
+                        const prevStart: number = prevMeasure.PositionAndShape.AbsolutePosition.x + prevMeasure.PositionAndShape.BorderMarginLeft + 0.4;
+                        const prevEnd: number = prevMeasure.PositionAndShape.AbsolutePosition.x + prevMeasure.PositionAndShape.BorderMarginRight;
+                        const prevMeasureSkyline: number = prevskyBottomLineCalculator.getSkyLineMinInRange(prevStart, prevEnd);
+                        //if prev skyline is higher, use it
+                        if (prevMeasureSkyline <= newSkylineValueForMeasure) {
+                            const skylineDifference: number = prevMeasureSkyline - newSkylineValueForMeasure;
+                            vexFlowVoltaHeight += skylineDifference;
+                            newSkylineValueForMeasure = prevMeasureSkyline;
+                        } else { //otherwise, we are higher. Need to adjust prev
+                            (nextStaveModifier as any).y_shift = vexFlowVoltaHeight * 10;
+                            prevMeasure.ParentStaffLine.SkyBottomLineCalculator.updateSkyLineInRange(prevStart, prevEnd, newSkylineValueForMeasure);
+                        }
+                    }
+                }
+            }
+
+            //convert to VF units (pixels)
+            vexFlowVoltaHeight *= 10;
+            this.stave.setVoltaType(voltaType, repetitionInstruction.endingIndices[0], vexFlowVoltaHeight);
+            skyBottomLineCalculator.updateSkyLineInRange(start, end, newSkylineValueForMeasure);
+        }
+    }
+
+    /**
+     * Sets the overall x-width of the measure.
+     * @param width
+     */
+    public setWidth(width: number): void {
+        super.setWidth(width);
+        // Set the width of the Vex.Flow.Stave
+        this.stave.setWidth(width * unitInPixels);
+        // Force the width of the Begin Instructions
+        //this.stave.setNoteStartX(this.beginInstructionsWidth * UnitInPixels);
+    }
+
+    /**
+     * This method is called after the StaffEntriesScaleFactor has been set.
+     * Here the final x-positions of the staff entries have to be set.
+     * (multiply the minimal positions with the scaling factor, considering the BeginInstructionsWidth)
+     */
+    public layoutSymbols(): void {
+        // vexflow does the x-layout
+    }
+
+    /**
+     * Draw this measure on a VexFlow CanvasContext
+     * @param ctx
+     */
+    public draw(ctx: Vex.IRenderContext): void {
+        // Draw stave lines
+        this.stave.setContext(ctx).draw();
+
+        this.multiRestElement.setStave(this.stave);
+        this.multiRestElement.setContext(ctx);
+        this.multiRestElement.draw();
+
+        // Draw vertical lines
+        for (const connector of this.connectors) {
+            connector.setContext(ctx).draw();
+        }
+    }
+
+    public format(): void {
+        // return
+    }
+
+    /**
+     * Returns all the voices that are present in this measure
+     */
+    public getVoicesWithinMeasure(): Voice[] {
+        return [];
+    }
+
+    /**
+     * Returns all the graphicalVoiceEntries of a given Voice.
+     * @param voice the voice for which the graphicalVoiceEntries shall be returned.
+     */
+    public getGraphicalVoiceEntriesPerVoice(voice: Voice): GraphicalVoiceEntry[] {
+        return [];
+    }
+
+    /**
+     * Finds the gaps between the existing notes within a measure.
+     * Problem here is, that the graphicalVoiceEntry does not exist yet and
+     * that Tied notes are not present in the normal voiceEntries.
+     * To handle this, calculation with absolute timestamps is needed.
+     * And the graphical notes have to be analysed directly (and not the voiceEntries, as it actually should be -> needs refactoring)
+     * @param voice the voice for which the ghost notes shall be searched.
+     */
+    protected getRestFilledVexFlowStaveNotesPerVoice(voice: Voice): GraphicalVoiceEntry[] {
+        return [];
+    }
+
+    /**
+     * Add a note to a beam
+     * @param graphicalNote
+     * @param beam
+     */
+    public handleBeam(graphicalNote: GraphicalNote, beam: Beam): void {
+        return;
+    }
+
+    public handleTuplet(graphicalNote: GraphicalNote, tuplet: Tuplet): void {
+        return;
+    }
+
+    /**
+     * Complete the creation of VexFlow Beams in this measure
+     */
+    public finalizeBeams(): void {
+        return;
+    }
+
+    /**
+     * Complete the creation of VexFlow Tuplets in this measure
+     */
+    public finalizeTuplets(): void {
+        return;
+    }
+
+    // this needs to exist, for some reason, or it won't be found, even though i can't find the usage.
+    public layoutStaffEntry(graphicalStaffEntry: GraphicalStaffEntry): void {
+        return;
+    }
+
+    public graphicalMeasureCreatedCalculations(): void {
+        return;
+    }
+
+
+    /**
+     * Create the articulations for all notes of the current staff entry
+     */
+    protected createArticulations(): void {
+        return;
+    }
+
+    /**
+     * Create the ornaments for all notes of the current staff entry
+     */
+    protected createOrnaments(): void {
+        return;
+    }
+
+    protected createFingerings(voiceEntry: GraphicalVoiceEntry): void {
+        return;
+    }
+
+    /**
+     * Creates a line from 'top' to this measure, of type 'lineType'
+     * @param top
+     * @param lineType
+     */
+    public lineTo(top: VexFlowMeasure, lineType: any): void {
+        const connector: StaveConnector = new Vex.Flow.StaveConnector(top.getVFStave(), this.stave);
+        connector.setType(lineType);
+        this.connectors.push(connector);
+    }
+
+    /**
+     * Return the VexFlow Stave corresponding to this graphicalMeasure
+     * @returns {Vex.Flow.Stave}
+     */
+    public getVFStave(): Vex.Flow.Stave {
+        return this.stave;
+    }
+
+    /**
+     * After re-running the formatting on the VexFlow Stave, update the
+     * space needed by Instructions (in VexFlow: StaveModifiers)
+     */
+    protected updateInstructionWidth(): void {
+        let vfBeginInstructionsWidth: number = 0;
+        let vfEndInstructionsWidth: number = 0;
+        const modifiers: Vex.Flow.StaveModifier[] = this.stave.getModifiers();
+        for (const mod of modifiers) {
+            if (mod.getPosition() === StavePositionEnum.BEGIN) {  //Vex.Flow.StaveModifier.Position.BEGIN) {
+                vfBeginInstructionsWidth += mod.getWidth() + mod.getPadding(undefined);
+            } else if (mod.getPosition() === StavePositionEnum.END) { //Vex.Flow.StaveModifier.Position.END) {
+                vfEndInstructionsWidth += mod.getWidth() + mod.getPadding(undefined);
+            }
+        }
+
+        this.beginInstructionsWidth = vfBeginInstructionsWidth / unitInPixels;
+        this.endInstructionsWidth = vfEndInstructionsWidth / unitInPixels;
+    }
+}
+
+// Gives the position of the Stave - replaces the function get Position() in the description of class StaveModifier in vexflow.d.ts
+// The latter gave an error because function cannot be defined in the class descriptions in vexflow.d.ts
+export enum StavePositionEnum {
+    LEFT = 1,
+    RIGHT = 2,
+    ABOVE = 3,
+    BELOW = 4,
+    BEGIN = 5,
+    END = 6
+}

+ 13 - 1
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -68,7 +68,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     MusicSheetCalculator.stafflineNoteCalculator = new VexflowStafflineNoteCalculator(this.rules);
     for (const graphicalMeasures of this.graphicalMusicSheet.MeasureList) {
       for (const graphicalMeasure of graphicalMeasures) {
-        (<VexFlowMeasure>graphicalMeasure).clean();
+        (<VexFlowMeasure>graphicalMeasure)?.clean();
       }
     }
   }
@@ -76,6 +76,9 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
   protected formatMeasures(): void {
     // let totalFinalizeBeamsTime: number = 0;
     for (const verticalMeasureList of this.graphicalMusicSheet.MeasureList) {
+      if (!verticalMeasureList || !verticalMeasureList[0]) {
+        continue;
+      }
       const firstMeasure: VexFlowMeasure = verticalMeasureList[0] as VexFlowMeasure;
       // first measure has formatting method as lambda function object, but formats all measures. TODO this could be refactored
       firstMeasure.format();
@@ -123,6 +126,9 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     const formatter: Vex.Flow.Formatter = new Vex.Flow.Formatter();
 
     for (const measure of measures) {
+      if (!measure) {
+        continue;
+      }
       const mvoices: { [voiceID: number]: Vex.Flow.Voice; } = (measure as VexFlowMeasure).vfVoices;
       const voices: Vex.Flow.Voice[] = [];
       for (const voiceID in mvoices) {
@@ -220,6 +226,9 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     }
 
     for (const graphicalMeasure of measures) {
+      if (!graphicalMeasure) {
+        continue;
+      }
       for (const staffEntry of graphicalMeasure.staffEntries) {
         // here the measure modifiers are not yet set, therefore the begin instruction width will be empty
         (<VexFlowStaffEntry>staffEntry).calculateXPosition();
@@ -247,6 +256,9 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     }
 
     for (const measure of measuresVertical) {
+      if (!measure) {
+        continue;
+      }
       const lastLyricEntryDict: LyricEntryDict = {}; // holds info about last lyrics entries for all verses j
 
       // for all staffEntries i, each containing the lyric entry for all verses at that timestamp in the measure

+ 2 - 0
src/MusicalScore/Interfaces/IGraphicalSymbolFactory.ts

@@ -25,6 +25,8 @@ export interface IGraphicalSymbolFactory {
 
     createGraphicalMeasure(sourceMeasure: SourceMeasure, staff: Staff): GraphicalMeasure;
 
+    createMultiRestMeasure(sourceMeasure: SourceMeasure, staff: Staff): GraphicalMeasure;
+
     createTabStaffMeasure(sourceMeasure: SourceMeasure, staff: Staff): GraphicalMeasure;
 
     createExtraGraphicalMeasure(staffLine: StaffLine): GraphicalMeasure;

+ 29 - 2
src/MusicalScore/ScoreIO/InstrumentReader.ts

@@ -26,7 +26,7 @@ import {SlurReader} from "./MusicSymbolModules/SlurReader";
 import {StemDirectionType} from "../VoiceData/VoiceEntry";
 import {NoteType, NoteTypeHandler} from "../VoiceData";
 import {SystemLinesEnumHelper} from "../Graphical";
-//import Dictionary from "typescript-collections/dist/lib/Dictionary";
+// import {Dictionary} from "typescript-collections";
 
 // FIXME: The following classes are missing
 //type ChordSymbolContainer = any;
@@ -411,7 +411,6 @@ export class InstrumentReader {
                 throw new MusicSheetReadingException(errorMsg + this.instrument.Name);
               }
             }
-
           }
           if (
             !xmlNode.element("divisions") &&
@@ -447,6 +446,34 @@ export class InstrumentReader {
               this.instrument.Staves[staffNumber - 1].StafflineCount = parseInt(staffLinesNode.value, 10);
             }
           }
+          // check multi measure rest
+          const measureStyle: IXmlElement = xmlNode.element("measure-style");
+          if (measureStyle) {
+            const multipleRest: IXmlElement = measureStyle.element("multiple-rest");
+            if (multipleRest) {
+              // TODO: save multirest per staff info a dictionary, to display a partial multirest if multirest values across staffs differ.
+              //   this makes the code bulkier though, and for now we only draw multirests if the staffs have the same multirest lengths.
+              // if (!currentMeasure.multipleRestMeasuresPerStaff) {
+              //   currentMeasure.multipleRestMeasuresPerStaff = new Dictionary<number, number>();
+              // }
+              const multipleRestValueXml: string = multipleRest.value;
+              let multipleRestNumber: number = 0;
+              try {
+                multipleRestNumber = Number.parseInt(multipleRestValueXml, 10);
+                if (currentMeasure.multipleRestMeasures !== undefined && multipleRestNumber !== currentMeasure.multipleRestMeasures) {
+                  // different multi-rest values in same measure for different staffs
+                  currentMeasure.multipleRestMeasures = 0; // for now, ignore multirest here. TODO: take minimum
+                  // currentMeasure.multipleRestMeasuresPerStaff.setValue(this.currentStaff?.Id, multipleRestNumber);
+                  //   issue: currentStaff can be undefined for first measure
+                } else {
+                  currentMeasure.multipleRestMeasures = multipleRestNumber;
+                }
+              } catch (e) {
+                console.log("multirest parse error: " + e);
+              }
+            }
+          }
+
         } else if (xmlNode.name === "forward") {
           const forFraction: number = parseInt(xmlNode.element("duration").value, 10);
           currentFraction.Add(new Fraction(forFraction, 4 * this.divisions));

+ 2 - 0
src/MusicalScore/VoiceData/SourceMeasure.ts

@@ -55,6 +55,8 @@ export class SourceMeasure {
     public printNewPageXml: boolean = false;
 
     private measureNumber: number;
+    public multipleRestMeasures: number; // usually undefined (0), unless "multiple-rest" given in XML (e.g. 4 measure rest)
+    // public multipleRestMeasuresPerStaff: Dictionary<number, number>; // key: staffId. value: how many rest measures
     private absoluteTimestamp: Fraction;
     private completeNumberOfStaves: number;
     private duration: Fraction;