Pārlūkot izejas kodu

Improvement/skyline (#270)

* feat(rendering): added sky and bottom lines
Benjamin Giesinger 7 gadi atpakaļ
vecāks
revīzija
b11f9e344d

+ 1 - 0
.appveyor.yml

@@ -1,5 +1,6 @@
 image: Visual Studio 2017
 environment:
+  timeout: 4000
   matrix:
     - nodejs_version: "6"
     # - nodejs_version: "7"

+ 2 - 0
.travis.yml

@@ -3,6 +3,8 @@ language: node_js
 node_js:
 - '6'
 - '8'
+env:
+  - timeout=4000
 notifications:
   email: false
   slack:

+ 57 - 30
demo/index.html

@@ -25,36 +25,40 @@
     </div>
     <div class="column">
         <h3 class="ui header">Render backend:</h3>
-        <select class="ui dropdown" id="backend-select" value="svg">
+        <select class="ui selection dropdown" id="backend-select" value="svg">
             <option value="svg">SVG</option>
             <option value="canvas">Canvas</option>>
         </select>
     </div>
     <div class="column">
         <h3 class="ui header">Cursor controls:</h3>
-        <div class="ui buttons">
-            <div class="ui animated fade button" id="show-cursor-btn">
-                <div class="visible content">Show</div>
-                <div class="hidden content">
-                    <i class="eye icon"></i>
+        <div>
+            <div class="ui vertical buttons">
+                <div class="ui animated fade button" id="show-cursor-btn">
+                    <div class="visible content">Show</div>
+                    <div class="hidden content">
+                        <i class="eye icon"></i>
+                    </div>
                 </div>
-            </div>
-            <div class="ui animated fade button" id="hide-cursor-btn">
-                <div class="visible content">Hide</div>
-                <div class="hidden content">
-                    <i class="eye slash icon"></i>
+                <div class="ui animated fade button" id="hide-cursor-btn">
+                    <div class="visible content">Hide</div>
+                    <div class="hidden content">
+                        <i class="eye slash icon"></i>
+                    </div>
                 </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 class="ui vertical buttons">
+                <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 animated fade button" id="reset-cursor-btn">
-                <div class="visible content">Reset</div>
-                <div class="hidden content">
-                    <i class="undo icon"></i>
+                <div class="ui animated fade button" id="reset-cursor-btn">
+                    <div class="visible content">Reset</div>
+                    <div class="hidden content">
+                        <i class="undo icon"></i>
+                    </div>
                 </div>
             </div>
         </div>
@@ -62,21 +66,44 @@
     <div class="column">
         <h3 class="ui header">Zoom controls:</h3>
         <div class="ui buttons">
-                <div class="ui button" id="zoom-in-btn">
-                    <i class="search plus icon"></i>
-                </div>
-                <div class="ui button" id="zoom-out-btn">
-                    <i class="search minus icon"></i>
-                </div>
+            <div class="ui button" id="zoom-in-btn">
+                <i class="search plus icon"></i>
+            </div>
+            <div class="ui button" id="zoom-out-btn">
+                <i class="search minus icon"></i>
+            </div>
         </div>
+        <h4 class="ui header" id="zoom-str">???</h4>
     </div>
     <div class="column">
-        <h3 class="ui header">Zoom factor:</h3>
-        <h4 class="ui header" id="zoom-str">???</h4>
+        <h3 class="ui header">Show bounding box for:</h3>
+        <select class="ui selection dropdown" id="selectBounding">
+            <option value="none">None</option>
+            <option value="all">All</option>
+            <option value="VexFlowMeasure">Measures</option>
+            <option value="VexFlowStaffEntry">Staff entries</option>
+            <option value="GraphicalLabel">Labels</option>
+            <option value="VexFlowStaffLine">Staff lines</option>
+            <option value="SystemLine">System lines</option>
+            <option value="StaffLineActivitySymbol">Activity symbols</option>
+        </select>
     </div>
     <div class="column">
-            <h3 class="ui header">Current width:</h3>                
-            <h4 class="ui header" id="size-str">???</h4>
+        <h3 class="ui header">Show debug information:</h3>
+        <div class="ui relaxed list">
+            <div class="item">
+                <div class="ui toggle checkbox">
+                    <input type="checkbox" name="public" id="skylineDebug">
+                    <label>Skyline</label>
+                </div>
+            </div>
+            <div class="item">
+                <div class="ui toggle checkbox">
+                    <input type="checkbox" name="public" id="bottomlineDebug">
+                    <label>Bottomline</label>
+                </div>
+            </div>
+        </div>
     </div>
 </div>
 <table cellspacing="0" style="max-width:700px;">

+ 21 - 5
demo/index.js

@@ -9,8 +9,6 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
     // The available demos
         demos = {
             "Beethoven - An die ferne Geliebte": "Beethoven_AnDieFerneGeliebte.xml",
-            "NinskaBanja_LoosMeasures.xml": "NinskaBanja_LoosMeasures.xml",
-            "NiskaBanja_DoesNotRender": "NiskaBanja_DoesNotRender.xml",
             "M. Clementi - Sonatina Op.36 No.1 Pt.1": "MuzioClementi_SonatinaOpus36No1_Part1.xml",
             "M. Clementi - Sonatina Op.36 No.1 Pt.2": "MuzioClementi_SonatinaOpus36No1_Part2.xml",
             "M. Clementi - Sonatina Op.36 No.3 Pt.1": "MuzioClementi_SonatinaOpus36No3_Part1.xml",
@@ -39,9 +37,11 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
         error_tr,
         canvas,
         select,
+        selectBounding,
+        skylineDebug,
+        bottomlineDebug,
         zoomIn,
         zoomOut,
-        size,
         zoomDiv,
         custom,
         nextCursorBtn,
@@ -56,10 +56,12 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
 
         err = document.getElementById("error-td");
         error_tr = document.getElementById("error-tr");
-        size = document.getElementById("size-str");
         zoomDiv = document.getElementById("zoom-str");
         custom = document.createElement("option");
         select = document.getElementById("select");
+        selectBounding = document.getElementById("selectBounding");
+        skylineDebug = document.getElementById("skylineDebug");
+        bottomlineDebug = document.getElementById("bottomlineDebug");
         zoomIn = document.getElementById("zoom-in-btn");
         zoomOut = document.getElementById("zoom-out-btn");
         canvas = document.createElement("div");
@@ -82,6 +84,7 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
             select.appendChild(option);
         }
         select.onchange = selectOnChange;
+        selectBounding.onchange = selectBoundingOnChange;
 
         // Pre-select default music piece
 
@@ -97,6 +100,14 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
             scale();
         };
 
+        skylineDebug.onclick = function() {
+            openSheetMusicDisplay.DrawSkyLine = !openSheetMusicDisplay.DrawSkyLine;
+        }
+
+        bottomlineDebug .onclick = function() {
+            openSheetMusicDisplay.DrawBottomLine = !openSheetMusicDisplay.DrawBottomLine;
+        }
+
         // Create OSMD object and canvas
         openSheetMusicDisplay = new OpenSheetMusicDisplay(canvas, false, backendSelect.value);
         openSheetMusicDisplay.setLogLevel('info');
@@ -175,6 +186,11 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
       window.setTimeout(endCallback, 1);
     }
 
+    function selectBoundingOnChange(evt) {
+        var value = evt.target.value;
+        openSheetMusicDisplay.DrawBoundingBox = value;
+    }
+
     function selectOnChange(str) {
         error();
         disable();
@@ -195,6 +211,7 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
             function() {
                 return onLoadingEnd(isCustom);
             }, function(e) {
+                console.warn(e.stack);
                 error("Error rendering sheet: " + process.env.DEBUG ? e.stack : e);
                 onLoadingEnd(isCustom);
             }
@@ -211,7 +228,6 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
     }
 
     function logCanvasSize() {
-        size.innerHTML = canvas.offsetWidth + "px";
         zoomDiv.innerHTML = Math.floor(zoom * 100.0) + "%";
     }
 

+ 6 - 0
external/vexflow/vexflow.d.ts

@@ -277,8 +277,14 @@ declare namespace Vex {
             public fillRect(x: number, y: number, width: number, height: number): RenderContext
             public fillText(text: string, x: number, y: number): RenderContext;
             public setFont(family: string, size: number, weight: string): RenderContext;
+            public beginPath(): RenderContext;
+            public moveTo(x, y): RenderContext;
+            public lineTo(x, y): RenderContext;
+            public closePath(): RenderContext;
+            public stroke(): RenderContext;
             public save(): RenderContext;
             public restore(): RenderContext;
+            public lineWidth: number;
         }
 
         export class CanvasContext extends RenderContext {

+ 4 - 1
karma.conf.js

@@ -85,7 +85,10 @@ module.exports = function (config) {
         logLevel: config.LOG_ERROR,
 
         client: {
-            captureConsole: true
+            captureConsole: true,
+            mocha: {
+                timeout: process.env.timeout || 2000
+            }
         },
 
         // enable / disable watching file and executing tests whenever any file changes

+ 11 - 0
src/MusicalScore/Graphical/GraphicalInstantaniousDynamicExpression.ts

@@ -0,0 +1,11 @@
+import { GraphicalObject } from "./GraphicalObject";
+import { InstantaniousDynamicExpression } from "../VoiceData/Expressions/InstantaniousDynamicExpression";
+
+export class GraphicalInstantaniousDynamicExpression extends GraphicalObject {
+
+    protected instantaniousDynamicExpression: InstantaniousDynamicExpression;
+
+    constructor(instantaniousDynamicExpression: InstantaniousDynamicExpression) {
+        super();
+    }
+}

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

@@ -288,6 +288,9 @@ export abstract class GraphicalStaffEntry extends GraphicalObject {
         }
     }
 
+    /**
+     * Returns true if this staff entry has only rests
+     */
     public hasOnlyRests(): boolean {
         const hasOnlyRests: boolean = true;
         for (const gve of this.graphicalVoiceEntries) {

+ 212 - 190
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -49,12 +49,13 @@ import {Staff} from "../VoiceData/Staff";
 import {OctaveShift} from "../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
 import * as log from "loglevel";
 import Dictionary from "typescript-collections/dist/lib/Dictionary";
-import {GraphicalLyricEntry} from "./GraphicalLyricEntry";
-import {GraphicalLyricWord} from "./GraphicalLyricWord";
-import {GraphicalLine} from "./GraphicalLine";
-import {Label} from "../Label";
+import { GraphicalLyricEntry } from "./GraphicalLyricEntry";
+import { GraphicalLyricWord } from "./GraphicalLyricWord";
+import { GraphicalLine } from "./GraphicalLine";
+import { Label } from "../Label";
 import { GraphicalVoiceEntry } from "./GraphicalVoiceEntry";
 import { VerticalSourceStaffEntryContainer } from "../VoiceData/VerticalSourceStaffEntryContainer";
+import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
 
 /**
  * Class used to do all the calculations in a MusicSheet, which in the end populates a GraphicalMusicSheet.
@@ -178,11 +179,6 @@ export abstract class MusicSheetCalculator {
         // create new MusicSystems and StaffLines (as many as necessary) and populate them with Measures from measureList
         this.calculateMusicSystems();
 
-        this.formatMeasures();
-
-        // calculate all LyricWords Positions
-        this.calculateLyricsPosition();
-
         // Add some white space at the end of the piece:
         this.graphicalMusicSheet.MusicPages[0].PositionAndShape.BorderMarginBottom += 9;
 
@@ -214,6 +210,44 @@ export abstract class MusicSheetCalculator {
     protected formatMeasures(): void {
         throw new Error("abstract, not implemented");
     }
+
+    /// <summary>
+    /// This method calculates the relative Positions of all MusicSystems.
+    /// </summary>
+    /// <param name="graphicalMusicPage"></param>
+    protected calculateMusicSystemsRelativePositions(graphicalMusicPage: GraphicalMusicPage): void {
+        // xPosition is always fix
+        let relativePosition: PointF2D = new PointF2D(this.rules.PageLeftMargin + this.rules.SystemLeftMargin, 0);
+
+        // first System is handled extra
+        const firstMusicSystem: MusicSystem = graphicalMusicPage.MusicSystems[0];
+        if (graphicalMusicPage === graphicalMusicPage.Parent.MusicPages[0]) {
+            relativePosition.y = this.rules.PageTopMargin + this.rules.TitleTopDistance + this.rules.SheetTitleHeight +
+                this.rules.TitleBottomDistance;
+        } else {
+            relativePosition.y = this.rules.PageTopMargin + this.rules.TitleTopDistance;
+        }
+        firstMusicSystem.PositionAndShape.RelativePosition = relativePosition;
+
+        for (let i: number = 1; i < graphicalMusicPage.MusicSystems.length; i++) {
+            const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[i];
+            relativePosition = new PointF2D(this.rules.PageLeftMargin + this.rules.SystemLeftMargin, 0);
+
+            // find optimum distance between Systems
+            const previousSystem: MusicSystem = graphicalMusicPage.MusicSystems[i - 1];
+            const lastPreviousStaffLine: StaffLine = previousSystem.StaffLines[previousSystem.StaffLines.length - 1];
+            const distance: number = (lastPreviousStaffLine.SkyBottomLineCalculator.getBottomLineMax() - this.rules.StaffHeight) +
+                Math.abs(musicSystem.StaffLines[0].SkyBottomLineCalculator.getSkyLineMin()) +
+                this.rules.MinimumAllowedDistanceBetweenSystems;
+
+            relativePosition.y = previousSystem.PositionAndShape.RelativePosition.y +
+                lastPreviousStaffLine.PositionAndShape.RelativePosition.y +
+                this.rules.StaffHeight + Math.max(this.rules.SystemDistance, distance);
+
+            musicSystem.PositionAndShape.RelativePosition = relativePosition;
+        }
+    }
+
     /**
      * Calculates the x layout of the staff entries within the staff measures belonging to one source measure.
      * All staff entries are x-aligned throughout all the measures.
@@ -223,6 +257,43 @@ export abstract class MusicSheetCalculator {
         throw new Error("abstract, not implemented");
     }
 
+    /**
+     * This method checks the distances between two System's StaffLines and if needed, shifts the lower down.
+     * @param musicSystem
+     */
+    protected optimizeDistanceBetweenStaffLines(musicSystem: MusicSystem): void {
+        musicSystem.PositionAndShape.calculateAbsolutePositionsRecursive(0, 0);
+
+        // don't perform any y-spacing in case of a StaffEntryLink (in both StaffLines)
+        if (!musicSystem.checkStaffEntriesForStaffEntryLink()) {
+            for (let i: number = 0; i < musicSystem.StaffLines.length - 1; i++) {
+                const upperBottomLine: number = musicSystem.StaffLines[i].SkyBottomLineCalculator.getBottomLineMax();
+                // TODO: Lower skyline should add to offset when there are items above the line. Currently no test
+                // file available
+                // const lowerSkyLine: number = Math.min(...musicSystem.StaffLines[i + 1].SkyLine);
+                if (Math.abs(upperBottomLine) > this.rules.MinimumStaffLineDistance) {
+                    // Remove staffheight from offset. As it results in huge distances
+                    const offset: number = Math.abs(upperBottomLine) + this.rules.MinimumStaffLineDistance - this.rules.StaffHeight;
+                    this.updateStaffLinesRelativePosition(musicSystem, i + 1, offset);
+                }
+            }
+        }
+    }
+
+    /**
+     * This method updates the System's StaffLine's RelativePosition (starting from the given index).
+     * @param musicSystem
+     * @param index
+     * @param value
+     */
+    protected updateStaffLinesRelativePosition(musicSystem: MusicSystem, index: number, value: number): void {
+        for (let i: number = index; i < musicSystem.StaffLines.length; i++) {
+            musicSystem.StaffLines[i].PositionAndShape.RelativePosition.y += value;
+        }
+
+        musicSystem.PositionAndShape.BorderBottom += value;
+    }
+
     protected calculateSystemYLayout(): void {
         throw new Error("abstract, not implemented");
     }
@@ -271,19 +342,19 @@ export abstract class MusicSheetCalculator {
         throw new Error("abstract, not implemented");
     }
 
-  /**
-   * Adds a technical instruction at the given staff entry.
-   * @param technicalInstructions
-   * @param voiceEntry
-   * @param staffEntry
-   */
-  protected handleVoiceEntryTechnicalInstructions(technicalInstructions: TechnicalInstruction[],
-                                                  voiceEntry: VoiceEntry, staffEntry: GraphicalStaffEntry): void {
-                                                    throw new Error("abstract, not implemented");
-  }
+    /**
+     * Adds a technical instruction at the given staff entry.
+     * @param technicalInstructions
+     * @param voiceEntry
+     * @param staffEntry
+     */
+    protected handleVoiceEntryTechnicalInstructions(technicalInstructions: TechnicalInstruction[],
+                                                    voiceEntry: VoiceEntry, staffEntry: GraphicalStaffEntry): void {
+        throw new Error("abstract, not implemented");
+    }
 
 
-  protected handleTuplet(graphicalNote: GraphicalNote, tuplet: Tuplet, openTuplets: Tuplet[]): void {
+    protected handleTuplet(graphicalNote: GraphicalNote, tuplet: Tuplet, openTuplets: Tuplet[]): void {
         throw new Error("abstract, not implemented");
     }
 
@@ -322,72 +393,68 @@ export abstract class MusicSheetCalculator {
                 !measure.parentSourceMeasure.ImplicitMeasure) {
                 if (measure.MeasureNumber !== 1 ||
                     (measure.MeasureNumber === 1 && measure !== staffLine.Measures[0])) {
-                        this.calculateSingleMeasureNumberPlacement(measure, staffLine, musicSystem);
-                    }
+                    this.calculateSingleMeasureNumberPlacement(measure, staffLine, musicSystem);
+                }
                 currentMeasureNumber = measure.MeasureNumber;
             }
         }
     }
 
-        /// <summary>
-        /// This method calculates a single MeasureNumberLabel and adds it to the graphical label list of the music system
-        /// </summary>
-        /// <param name="measure"></param>
-        /// <param name="staffLine"></param>
-        /// <param name="musicSystem"></param>
-        private calculateSingleMeasureNumberPlacement(measure: GraphicalMeasure, staffLine: StaffLine, musicSystem: MusicSystem): void {
-            const labelNumber: string = measure.MeasureNumber.toString();
-            const graphicalLabel: GraphicalLabel = new GraphicalLabel(new Label(labelNumber), this.rules.MeasureNumberLabelHeight,
-                                                                      TextAlignment.LeftBottom);
-            // FIXME: Change if Skyline is available
-            // const skyBottomLineCalculator: SkyBottomLineCalculator = new SkyBottomLineCalculator(this.rules);
-
-            // calculate LabelBoundingBox and set PSI parent
-            graphicalLabel.setLabelPositionAndShapeBorders();
-            graphicalLabel.PositionAndShape.Parent = musicSystem.PositionAndShape;
-
-            // calculate relative Position
-            const relativeX: number = staffLine.PositionAndShape.RelativePosition.x +
-                              measure.PositionAndShape.RelativePosition.x - graphicalLabel.PositionAndShape.BorderMarginLeft;
-            let relativeY: number;
-
-            // and the corresponding SkyLine indeces
-            // tslint:disable-next-line:no-unused-variable
-            let start: number = relativeX;
-            // tslint:disable-next-line:no-unused-variable
-            let end: number = relativeX - graphicalLabel.PositionAndShape.BorderLeft + graphicalLabel.PositionAndShape.BorderMarginRight;
-
-            // take into account the InstrumentNameLabel's at the beginning of the first MusicSystem
-            if (staffLine === musicSystem.StaffLines[0] && musicSystem === musicSystem.Parent.MusicSystems[0]) {
-                start -= staffLine.PositionAndShape.RelativePosition.x;
-                end -= staffLine.PositionAndShape.RelativePosition.x;
-            }
-
-            // get the minimum corresponding SkyLine value
-            // FIXME: Change if Skyline is available
-            // const skyLineMinValue: number = skyBottomLineCalculator.getSkyLineMinInRange(staffLine, start, end);
-            const skyLineMinValue: number = 0;
-
-            if (measure === staffLine.Measures[0]) {
-                // must take into account possible MusicSystem Bracket's
-                let minBracketTopBorder: number = 0;
-                if (musicSystem.GroupBrackets.length > 0) {
-                    for (const groupBracket of musicSystem.GroupBrackets) {
-                        minBracketTopBorder = Math.min(minBracketTopBorder, groupBracket.PositionAndShape.BorderTop);
-                    }
+    /// <summary>
+    /// This method calculates a single MeasureNumberLabel and adds it to the graphical label list of the music system
+    /// </summary>
+    /// <param name="measure"></param>
+    /// <param name="staffLine"></param>
+    /// <param name="musicSystem"></param>
+    private calculateSingleMeasureNumberPlacement(measure: GraphicalMeasure, staffLine: StaffLine, musicSystem: MusicSystem): void {
+        const labelNumber: string = measure.MeasureNumber.toString();
+        const graphicalLabel: GraphicalLabel = new GraphicalLabel(new Label(labelNumber), this.rules.MeasureNumberLabelHeight,
+                                                                  TextAlignment.LeftBottom);
+
+        const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
+
+        // calculate LabelBoundingBox and set PSI parent
+        graphicalLabel.setLabelPositionAndShapeBorders();
+        graphicalLabel.PositionAndShape.Parent = musicSystem.PositionAndShape;
+
+        // calculate relative Position
+        const relativeX: number = staffLine.PositionAndShape.RelativePosition.x +
+            measure.PositionAndShape.RelativePosition.x - graphicalLabel.PositionAndShape.BorderMarginLeft;
+        let relativeY: number;
+
+        // and the corresponding SkyLine indeces
+        let start: number = relativeX;
+        let end: number = relativeX - graphicalLabel.PositionAndShape.BorderLeft + graphicalLabel.PositionAndShape.BorderMarginRight;
+
+        // take into account the InstrumentNameLabel's at the beginning of the first MusicSystem
+        if (staffLine === musicSystem.StaffLines[0] && musicSystem === musicSystem.Parent.MusicSystems[0]) {
+            start -= staffLine.PositionAndShape.RelativePosition.x;
+            end -= staffLine.PositionAndShape.RelativePosition.x;
+        }
+
+        // get the minimum corresponding SkyLine value
+        const skyLineMinValue: number = skyBottomLineCalculator.getSkyLineMinInRange(start, end);
+
+        if (measure === staffLine.Measures[0]) {
+            // must take into account possible MusicSystem Bracket's
+            let minBracketTopBorder: number = 0;
+            if (musicSystem.GroupBrackets.length > 0) {
+                for (const groupBracket of musicSystem.GroupBrackets) {
+                    minBracketTopBorder = Math.min(minBracketTopBorder, groupBracket.PositionAndShape.BorderTop);
                 }
-                relativeY = Math.min(skyLineMinValue, minBracketTopBorder);
-            } else {
-                relativeY = skyLineMinValue;
             }
+            relativeY = Math.min(skyLineMinValue, minBracketTopBorder);
+        } else {
+            relativeY = skyLineMinValue;
+        }
 
-            relativeY = Math.min(0, relativeY);
+        relativeY = Math.min(0, relativeY);
 
-            graphicalLabel.PositionAndShape.RelativePosition = new PointF2D(relativeX, relativeY);
-            // FIXME: Change if Skyline is available
-            // skyBottomLineCalculator.updateSkyLineInRange(staffLine, start, end, relativeY + graphicalLabel.PositionAndShape.BorderMarginTop);
-            musicSystem.MeasureNumberLabels.push(graphicalLabel);
-        }
+        graphicalLabel.PositionAndShape.RelativePosition = new PointF2D(relativeX, relativeY);
+
+        skyBottomLineCalculator.updateSkyLineInRange(start, end, relativeY + graphicalLabel.PositionAndShape.BorderMarginTop);
+        musicSystem.MeasureNumberLabels.push(graphicalLabel);
+    }
 
     /**
      * Calculate the shape (Bézier curve) for this tie.
@@ -398,8 +465,6 @@ export abstract class MusicSheetCalculator {
         throw new Error("abstract, not implemented");
     }
 
-    // FIXME: There are several HACKS in this function to make multiline lyrics work without the skyline.
-    // These need to be reverted once the skyline is available
     /**
      * Calculate the Lyrics YPositions for a single [[StaffLine]].
      * @param staffLine
@@ -407,11 +472,9 @@ export abstract class MusicSheetCalculator {
      */
     protected calculateSingleStaffLineLyricsPosition(staffLine: StaffLine, lyricVersesNumber: number[]): GraphicalStaffEntry[] {
         let numberOfVerses: number = 0;
-        // FIXME: There is no class SkyBottomLineCalculator -> Fix value
-        // TODO make lyric offset dynamic (also see above)
-        let lyricsStartYPosition: number = this.rules.StaffHeight + this.rules.LyricsYOffsetToStaffHeight; // Add offset to prevent collision
+        let lyricsStartYPosition: number = this.rules.StaffHeight; // Add offset to prevent collision
         const lyricsStaffEntriesList: GraphicalStaffEntry[] = [];
-        // const skyBottomLineCalculator: SkyBottomLineCalculator = new SkyBottomLineCalculator(this.rules);
+        const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
 
         // first find maximum Ycoordinate for the whole StaffLine
         let len: number = staffLine.Measures.length;
@@ -427,7 +490,7 @@ export abstract class MusicSheetCalculator {
 
                     // Position of Staffentry relative to StaffLine
                     const staffEntryPositionX: number = staffEntry.PositionAndShape.RelativePosition.x +
-                                                        measureRelativePosition.x;
+                        measureRelativePosition.x;
 
                     let minMarginLeft: number = Number.MAX_VALUE;
                     let maxMarginRight: number = Number.MAX_VALUE;
@@ -440,19 +503,17 @@ export abstract class MusicSheetCalculator {
                     }
 
                     // check BottomLine in this range and take the maximum between the two values
-                    // FIXME: There is no class SkyBottomLineCalculator -> Fix value
-                    // float bottomLineMax = skyBottomLineCalculator.getBottomLineMaxInRange(staffLine, minMarginLeft, maxMarginRight);
-                    const bottomLineMax: number = 0.0;
+                    const bottomLineMax: number = skyBottomLineCalculator.getBottomLineMaxInRange(minMarginLeft, maxMarginRight)
+                        + this.rules.StaffHeight;
                     lyricsStartYPosition = Math.max(lyricsStartYPosition, bottomLineMax);
                 }
             }
         }
 
-        let maxPosition: number = 4.0;
+        let maxPosition: number = 0;
         // iterate again through the Staffentries with LyricEntries
         len = lyricsStaffEntriesList.length;
-        for (let idx: number = 0; idx < len; ++idx) {
-            const staffEntry: GraphicalStaffEntry = lyricsStaffEntriesList[idx];
+        for (const staffEntry of lyricsStaffEntriesList) {
             // set LyricEntryLabel RelativePosition
             for (let i: number = 0; i < staffEntry.LyricsEntries.length; i++) {
                 const lyricEntry: GraphicalLyricEntry = staffEntry.LyricsEntries[i];
@@ -462,10 +523,10 @@ export abstract class MusicSheetCalculator {
                 // eg verseNumbers: 2,3,4,6 => 1,2,3,4
                 const verseNumber: number = lyricEntry.GetLyricsEntry.VerseNumber;
                 const sortedLyricVerseNumberIndex: number = lyricVersesNumber.indexOf(verseNumber);
-                const firstPosition: number = lyricsStartYPosition + this.rules.LyricsHeight;
+                const firstPosition: number = lyricsStartYPosition + this.rules.LyricsHeight + this.rules.VerticalBetweenLyricsDistance;
 
                 // Y-position calculated according to aforementioned mapping
-                let position: number = firstPosition + (this.rules.VerticalBetweenLyricsDistance + this.rules.LyricsHeight) * (sortedLyricVerseNumberIndex);
+                let position: number = firstPosition + (this.rules.VerticalBetweenLyricsDistance + this.rules.LyricsHeight) * sortedLyricVerseNumberIndex;
                 if (this.leadSheet) {
                     position = 3.4 + (this.rules.VerticalBetweenLyricsDistance + this.rules.LyricsHeight) * (sortedLyricVerseNumberIndex);
                 }
@@ -476,41 +537,11 @@ export abstract class MusicSheetCalculator {
 
         // update BottomLine (on the whole StaffLine's length)
         if (lyricsStaffEntriesList.length > 0) {
-            /**
-             * HACK START
-             */
-            let additionalPageLength: number = 0;
-            maxPosition -= this.rules.StaffHeight;
-            let iterator: StaffLine = staffLine.NextStaffLine;
-            let systemMaxCount: number = 0;
-            while (iterator !== undefined) {
-                iterator.PositionAndShape.RelativePosition.y += maxPosition;
-                iterator = iterator.NextStaffLine;
-                systemMaxCount += maxPosition;
-                additionalPageLength += maxPosition;
-            }
-            // Special care for single line systems
-            if (systemMaxCount !== 0) {
-                systemMaxCount -= this.rules.BetweenStaffDistance;
-                let systemIterator: MusicSystem = staffLine.ParentMusicSystem.NextSystem;
-                while (systemIterator !== undefined) {
-                    systemIterator.PositionAndShape.RelativePosition.y += systemMaxCount;
-                    systemIterator = systemIterator.NextSystem;
-                    additionalPageLength += systemMaxCount;
-                }
-                staffLine.ParentMusicSystem.Parent.PositionAndShape.BorderBottom += additionalPageLength;
-                // Update the instrument labels
-            }
-            staffLine.ParentMusicSystem.setMusicSystemLabelsYPosition();
-            /**
-             * HACK END
-             */
-            // const endX: number = staffLine.PositionAndShape.Size.width;
-            // const startX: number = lyricsStaffEntriesList[0].PositionAndShape.RelativePosition.x +
-            // lyricsStaffEntriesList[0].PositionAndShape.BorderMarginLeft +
-            // lyricsStaffEntriesList[0].parentMeasure.PositionAndShape.RelativePosition.x;
-            // FIXME: There is no class SkyBottomLineCalculator. This call should update the positions according to the last run
-            // skyBottomLineCalculator.updateBottomLineInRange(staffLine, startX, endX, maxPosition);
+            const endX: number = staffLine.PositionAndShape.Size.width;
+            const startX: number = lyricsStaffEntriesList[0].PositionAndShape.RelativePosition.x +
+                lyricsStaffEntriesList[0].PositionAndShape.BorderMarginLeft +
+                lyricsStaffEntriesList[0].parentMeasure.PositionAndShape.RelativePosition.x;
+            skyBottomLineCalculator.updateBottomLineInRange(startX, endX, maxPosition);
         }
         return lyricsStaffEntriesList;
     }
@@ -674,9 +705,6 @@ export abstract class MusicSheetCalculator {
         if (!this.leadSheet) {
             this.calculateOrnaments();
         }
-        // update Sky- and BottomLine with borderValues 0.0 and 4.0 respectively
-        // (must also come after Slurs)
-        this.updateSkyBottomLines();
         // calculate StaffEntry ChordSymbols
         this.calculateChordSymbols();
         if (!this.leadSheet) {
@@ -698,6 +726,11 @@ export abstract class MusicSheetCalculator {
             this.calculateTempoExpressions();
         }
 
+        this.formatMeasures();
+
+        // calculate all LyricWords Positions
+        this.calculateLyricsPosition();
+
         // update all StaffLine's Borders
         // create temporary Object, just to call the methods (in order to avoid declaring them static)
         for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
@@ -710,10 +743,11 @@ export abstract class MusicSheetCalculator {
                 }
             }
         }
-        // calculate Comments for each Staffline
-        this.calculateComments();
+
         // Y-spacing
         this.calculateSystemYLayout();
+        // calculate Comments for each Staffline
+        this.calculateComments();
         // calculate marked Areas for Systems
         this.calculateMarkedAreas();
 
@@ -755,21 +789,12 @@ export abstract class MusicSheetCalculator {
             if (graphicalMusicPage === this.graphicalMusicSheet.MusicPages[0]) {
                 this.calculatePageLabels(graphicalMusicPage);
             }
+
             // calculate TopBottom Borders for all elements recursively
             graphicalMusicPage.PositionAndShape.calculateTopBottomBorders();
         }
     }
 
-    protected updateSkyBottomLine(staffLine: StaffLine): void {
-        //log.debug("updateSkyBottomLine not implemented");
-        return;
-    }
-
-    protected calculateSkyBottomLine(staffLine: StaffLine): void {
-        //log.debug("calculateSkyBottomLine not implemented");
-        return;
-    }
-
     protected calculateMarkedAreas(): void {
         //log.debug("calculateMarkedAreas not implemented");
         return;
@@ -1366,7 +1391,7 @@ export abstract class MusicSheetCalculator {
                 let octaveShiftValue: OctaveEnum = OctaveEnum.NONE;
                 if (openOctaveShifts[staffIndex] !== undefined) {
                     const octaveShiftParams: OctaveShiftParams = openOctaveShifts[staffIndex];
-                    if (octaveShiftParams.getAbsoluteStartTimestamp.lte(sourceStaffEntry.AbsoluteTimestamp)  &&
+                    if (octaveShiftParams.getAbsoluteStartTimestamp.lte(sourceStaffEntry.AbsoluteTimestamp) &&
                         sourceStaffEntry.AbsoluteTimestamp.lte(octaveShiftParams.getAbsoluteEndTimestamp)) {
                         octaveShiftValue = octaveShiftParams.getOpenOctaveShift.Type;
                     }
@@ -1425,9 +1450,9 @@ export abstract class MusicSheetCalculator {
         // if there are no staffEntries in this measure, create a rest for the whole measure:
         if (measure.staffEntries.length === 0) {
             const sourceStaffEntry: SourceStaffEntry = new SourceStaffEntry(
-                new VerticalSourceStaffEntryContainer(  measure.parentSourceMeasure,
-                                                        measure.parentSourceMeasure.AbsoluteTimestamp,
-                                                        measure.parentSourceMeasure.CompleteNumberOfStaves),
+                new VerticalSourceStaffEntryContainer(measure.parentSourceMeasure,
+                                                      measure.parentSourceMeasure.AbsoluteTimestamp,
+                                                      measure.parentSourceMeasure.CompleteNumberOfStaves),
                 staff);
             const voiceEntry: VoiceEntry = new VoiceEntry(new Fraction(0, 1), staff.Voices[0], sourceStaffEntry);
             const note: Note = new Note(voiceEntry, sourceStaffEntry, Fraction.createFromFraction(sourceMeasure.Duration), undefined);
@@ -1437,10 +1462,10 @@ export abstract class MusicSheetCalculator {
             graphicalStaffEntry.relInMeasureTimestamp = voiceEntry.Timestamp;
             const gve: GraphicalVoiceEntry = MusicSheetCalculator.symbolFactory.createVoiceEntry(voiceEntry, graphicalStaffEntry);
             graphicalStaffEntry.graphicalVoiceEntries.push(gve);
-            const graphicalNote: GraphicalNote = MusicSheetCalculator.symbolFactory.createNote( note,
-                                                                                                gve,
-                                                                                                new ClefInstruction(),
-                                                                                                OctaveEnum.NONE, undefined);
+            const graphicalNote: GraphicalNote = MusicSheetCalculator.symbolFactory.createNote(note,
+                                                                                               gve,
+                                                                                               new ClefInstruction(),
+                                                                                               OctaveEnum.NONE, undefined);
             gve.notes.push(graphicalNote);
         }
         return measure;
@@ -1459,18 +1484,18 @@ export abstract class MusicSheetCalculator {
         accidentalCalculator.checkAccidental(graphicalNote, pitch);
     }
 
-    private updateSkyBottomLines(): void {
-        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-            const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-            for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
-                const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
-                for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
-                    const staffLine: StaffLine = musicSystem.StaffLines[idx3];
-                    this.updateSkyBottomLine(staffLine);
-                }
-            }
-        }
-    }
+    // // needed to disable linter, as it doesn't recognize the existing usage of this method.
+    // // ToDo: check if a newer version doesn't have the problem.
+    // /* tslint:disable:no-unused-variable */
+    // private createStaffEntryForTieNote(measure: StaffMeasure, absoluteTimestamp: Fraction, openTie: Tie): GraphicalStaffEntry {
+    //     /* tslint:enable:no-unused-variable */
+    //     let graphicalStaffEntry: GraphicalStaffEntry;
+    //     graphicalStaffEntry = MusicSheetCalculator.symbolFactory.createStaffEntry(openTie.Start.ParentStaffEntry, measure);
+    //     graphicalStaffEntry.relInMeasureTimestamp = Fraction.minus(absoluteTimestamp, measure.parentSourceMeasure.AbsoluteTimestamp);
+    //     this.resetYPositionForLeadSheet(graphicalStaffEntry.PositionAndShape);
+    //     measure.addGraphicalStaffEntryAtTimestamp(graphicalStaffEntry);
+    //     return graphicalStaffEntry;
+    // }
 
     private handleStaffEntries(): void {
         for (let idx: number = 0, len: number = this.graphicalMusicSheet.MeasureList.length; idx < len; ++idx) {
@@ -1490,13 +1515,10 @@ export abstract class MusicSheetCalculator {
     }
 
     private calculateSkyBottomLines(): void {
-        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-            const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-            for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
-                const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
-                for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
-                    const staffLine: StaffLine = musicSystem.StaffLines[idx3];
-                    this.calculateSkyBottomLine(staffLine);
+        for (const graphicalMusicPage of this.graphicalMusicSheet.MusicPages) {
+            for (const musicSystem of graphicalMusicPage.MusicSystems) {
+                for (const staffLine of musicSystem.StaffLines) {
+                    staffLine.SkyBottomLineCalculator.calculateLines();
                 }
             }
         }
@@ -1739,11 +1761,11 @@ export abstract class MusicSheetCalculator {
         if (lyricEntry.StaffEntryParent.parentMeasure.ParentStaffLine === nextLyricEntry.StaffEntryParent.parentMeasure.ParentStaffLine) {
             // start- and End margins from the text Labels
             const startX: number = startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
-            startStaffEntry.PositionAndShape.RelativePosition.x +
-            lyricEntry.GraphicalLabel.PositionAndShape.BorderMarginRight;
+                startStaffEntry.PositionAndShape.RelativePosition.x +
+                lyricEntry.GraphicalLabel.PositionAndShape.BorderMarginRight;
             const endX: number = endStaffentry.parentMeasure.PositionAndShape.RelativePosition.x +
-            endStaffentry.PositionAndShape.RelativePosition.x +
-            nextLyricEntry.GraphicalLabel.PositionAndShape.BorderMarginLeft;
+                endStaffentry.PositionAndShape.RelativePosition.x +
+                nextLyricEntry.GraphicalLabel.PositionAndShape.BorderMarginLeft;
             const y: number = lyricEntry.GraphicalLabel.PositionAndShape.RelativePosition.y;
             let numberOfDashes: number = 1;
             if ((endX - startX) > this.rules.BetweenSyllabelMaximumDistance) {
@@ -1774,7 +1796,7 @@ export abstract class MusicSheetCalculator {
 
             // calculate Dashes for the second StaffLine (only if endStaffEntry isn't the first StaffEntry of the StaffLine)
             if (!(endStaffentry === endStaffentry.parentMeasure.staffEntries[0] &&
-                    endStaffentry.parentMeasure === endStaffentry.parentMeasure.ParentStaffLine.Measures[0])) {
+                endStaffentry.parentMeasure === endStaffentry.parentMeasure.ParentStaffLine.Measures[0])) {
                 const secondStartX: number = nextStaffLine.Measures[0].staffEntries[0].PositionAndShape.RelativePosition.x;
                 const secondEndX: number = endStaffentry.parentMeasure.PositionAndShape.RelativePosition.x +
                     endStaffentry.PositionAndShape.RelativePosition.x +
@@ -1852,8 +1874,8 @@ export abstract class MusicSheetCalculator {
         let endStaffLine: StaffLine = undefined;
         const staffIndex: number = startStaffEntry.parentMeasure.ParentStaff.idInMusicSheet;
         for (let index: number = startStaffEntry.parentVerticalContainer.Index + 1;
-             index < this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers.length;
-             ++index) {
+            index < this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers.length;
+            ++index) {
             const gse: GraphicalStaffEntry = this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[index].StaffEntries[staffIndex];
             if (gse === undefined) {
                 continue;
@@ -1900,7 +1922,7 @@ export abstract class MusicSheetCalculator {
             }
             // second Underscore in the endStaffLine until endStaffEntry (if endStaffEntry isn't the first StaffEntry of the StaffLine))
             if (!(endStaffEntry === endStaffEntry.parentMeasure.staffEntries[0] &&
-                    endStaffEntry.parentMeasure === endStaffEntry.parentMeasure.ParentStaffLine.Measures[0])) {
+                endStaffEntry.parentMeasure === endStaffEntry.parentMeasure.ParentStaffLine.Measures[0])) {
                 const secondStartX: number = endStaffLine.Measures[0].staffEntries[0].PositionAndShape.RelativePosition.x;
                 const secondEndX: number = endStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
                     endStaffEntry.PositionAndShape.RelativePosition.x +
@@ -1935,21 +1957,21 @@ export abstract class MusicSheetCalculator {
      * @param {number} y
      * @returns {number}
      */
-    private calculateRightAndLeftDashesForLyricWord (staffLine: StaffLine, startX: number, endX: number, y: number): number {
-        const leftDash: GraphicalLabel = new GraphicalLabel (new Label ("-"), this.rules.LyricsHeight, TextAlignment.CenterBottom);
+    private calculateRightAndLeftDashesForLyricWord(staffLine: StaffLine, startX: number, endX: number, y: number): number {
+        const leftDash: GraphicalLabel = new GraphicalLabel(new Label("-"), this.rules.LyricsHeight, TextAlignment.CenterBottom);
         leftDash.setLabelPositionAndShapeBorders();
         staffLine.LyricsDashes.push(leftDash);
         if (this.staffLinesWithLyricWords.indexOf(staffLine) === -1) {
             this.staffLinesWithLyricWords.push(staffLine);
         }
         leftDash.PositionAndShape.Parent = staffLine.PositionAndShape;
-        const leftDashRelative: PointF2D = new PointF2D (startX, y);
+        const leftDashRelative: PointF2D = new PointF2D(startX, y);
         leftDash.PositionAndShape.RelativePosition = leftDashRelative;
-        const rightDash: GraphicalLabel = new GraphicalLabel (new Label ("-"), this.rules.LyricsHeight, TextAlignment.CenterBottom);
+        const rightDash: GraphicalLabel = new GraphicalLabel(new Label("-"), this.rules.LyricsHeight, TextAlignment.CenterBottom);
         rightDash.setLabelPositionAndShapeBorders();
         staffLine.LyricsDashes.push(rightDash);
         rightDash.PositionAndShape.Parent = staffLine.PositionAndShape;
-        const rightDashRelative: PointF2D = new PointF2D (endX, y);
+        const rightDashRelative: PointF2D = new PointF2D(endX, y);
         rightDash.PositionAndShape.RelativePosition = rightDashRelative;
         return (rightDash.PositionAndShape.RelativePosition.x - leftDash.PositionAndShape.RelativePosition.x);
     }
@@ -1962,8 +1984,8 @@ export abstract class MusicSheetCalculator {
                     for (let k: number = 0; k < sourceMeasure.StaffLinkedExpressions[j].length; k++) {
                         if (sourceMeasure.StaffLinkedExpressions[j][k].InstantaniousDynamic !== undefined ||
                             (sourceMeasure.StaffLinkedExpressions[j][k].StartingContinuousDynamic !== undefined &&
-                            sourceMeasure.StaffLinkedExpressions[j][k].StartingContinuousDynamic.StartMultiExpression ===
-                            sourceMeasure.StaffLinkedExpressions[j][k] && sourceMeasure.StaffLinkedExpressions[j][k].UnknownList.length === 0)
+                                sourceMeasure.StaffLinkedExpressions[j][k].StartingContinuousDynamic.StartMultiExpression ===
+                                sourceMeasure.StaffLinkedExpressions[j][k] && sourceMeasure.StaffLinkedExpressions[j][k].UnknownList.length === 0)
                         ) {
                             this.calculateDynamicExpressionsForSingleMultiExpression(sourceMeasure.StaffLinkedExpressions[j][k], i, j);
                         }
@@ -2071,13 +2093,13 @@ export abstract class MusicSheetCalculator {
         if (hasLink) {
             // in case of StaffEntryLink don't check mainVoice / linkedVoice
             if (voiceEntry === voiceEntry.ParentSourceStaffEntry.VoiceEntries[0]) {
-            // set stem up:
-            voiceEntry.StemDirection = StemDirectionType.Up;
-            return;
+                // set stem up:
+                voiceEntry.StemDirection = StemDirectionType.Up;
+                return;
             } else {
-            // set stem down:
-            voiceEntry.StemDirection = StemDirectionType.Down;
-            return;
+                // set stem down:
+                voiceEntry.StemDirection = StemDirectionType.Down;
+                return;
             }
         } else {
             if (voiceEntry.ParentVoice instanceof LinkedVoice) {

+ 15 - 3
src/MusicalScore/Graphical/MusicSheetDrawer.ts

@@ -41,6 +41,10 @@ export abstract class MusicSheetDrawer {
     public drawingParameters: DrawingParameters = new DrawingParameters();
     public splitScreenLineColor: number;
     public midiPlaybackAvailable: boolean;
+    public drawableBoundingBoxElement: string = process.env.DRAW_BOUNDING_BOX_ELEMENT;
+
+    public skyLineVisible: boolean = false;
+    public bottomLineVisible: boolean = false;
 
     protected rules: EngravingRules;
     protected graphicalMusicSheet: GraphicalMusicSheet;
@@ -338,6 +342,14 @@ export abstract class MusicSheetDrawer {
         if (staffLine.LyricsDashes.length > 0) {
             this.drawDashes(staffLine.LyricsDashes);
         }
+
+        if (this.skyLineVisible) {
+            this.drawSkyLine(staffLine);
+        }
+
+        if (this.bottomLineVisible) {
+            this.drawBottomLine(staffLine);
+        }
     }
 
     /**
@@ -433,8 +445,8 @@ export abstract class MusicSheetDrawer {
         }
         // Draw bounding boxes for debug purposes. This has to be at the end because only
         // then all the calculations and recalculations are done
-        if (process.env.DRAW_BOUNDING_BOX_ELEMENT) {
-            this.drawBoundingBoxes(page.PositionAndShape, 0, process.env.DRAW_BOUNDING_BOX_ELEMENT);
+        if (this.drawableBoundingBoxElement) {
+            this.drawBoundingBoxes(page.PositionAndShape, 0, this.drawableBoundingBoxElement);
         }
 
     }
@@ -454,7 +466,7 @@ export abstract class MusicSheetDrawer {
                                                          (relBoundingRect.width + 0), (relBoundingRect.height + 0));
             tmpRect = this.applyScreenTransformationForRect(tmpRect);
             this.renderRectangle(tmpRect, <number>GraphicalLayers.Background, layer, 0.5);
-            this.renderLabel(new GraphicalLabel(new Label(dataObjectString), 1.2, TextAlignment.CenterCenter),
+            this.renderLabel(new GraphicalLabel(new Label(dataObjectString), 0.8, TextAlignment.CenterCenter),
                              layer, tmpRect.width, tmpRect.height, tmpRect.height, new PointF2D(tmpRect.x, tmpRect.y + 12));
         }
         layer++;

+ 498 - 0
src/MusicalScore/Graphical/SkyBottomLineCalculator.ts

@@ -0,0 +1,498 @@
+import {EngravingRules} from "./EngravingRules";
+import {StaffLine} from "./StaffLine";
+import {PointF2D} from "../../Common/DataObjects/PointF2D";
+import {CanvasVexFlowBackend} from "./VexFlow/CanvasVexFlowBackend";
+import {VexFlowMeasure} from "./VexFlow/VexFlowMeasure";
+import {unitInPixels} from "./VexFlow/VexFlowMusicSheetDrawer";
+import * as log from "loglevel";
+/**
+ * This class calculates and holds the skyline and bottom line information.
+ * It also has functions to update areas of the two lines if new elements are
+ * added to the staffline (e.g. measure number, annotations, ...)
+ */
+export class SkyBottomLineCalculator {
+    /** Parent Staffline where the skyline and bottom line is attached */
+    private mStaffLineParent: StaffLine;
+    /** Internal array for the skyline */
+    private mSkyLine: number[];
+    /** Internal array for the bottomline */
+    private mBottomLine: number[];
+    /** Engraving rules for formatting */
+    private mRules: EngravingRules;
+
+    /**
+     * Create a new object of the calculator
+     * @param staffLineParent staffline where the calculator should be attached
+     */
+    constructor(staffLineParent: StaffLine) {
+        this.mStaffLineParent = staffLineParent;
+        this.mRules = EngravingRules.Rules;
+    }
+
+    /**
+     * This method calculates the Sky- and BottomLines for a StaffLine.
+     */
+    public calculateLines(): void {
+        // calculate arrayLength
+        const arrayLength: number = Math.max(Math.ceil(this.StaffLineParent.PositionAndShape.Size.width * this.SamplingUnit), 1);
+        this.mSkyLine = [];
+        this.mBottomLine = [];
+
+        // Create a temporary canvas outside the DOM to draw the measure in.
+        const tmpCanvas: any = new CanvasVexFlowBackend();
+        // search through all Measures
+        for (const measure of this.StaffLineParent.Measures as VexFlowMeasure[]) {
+            // must calculate first AbsolutePositions
+            measure.PositionAndShape.calculateAbsolutePositionsRecursive(0, 0);
+
+            // Pre initialize and get stuff for more performance
+            const vsStaff: any = measure.getVFStave();
+            // Headless because we are outside the DOM
+            tmpCanvas.initializeHeadless(vsStaff.getWidth());
+            const ctx: any = tmpCanvas.getContext();
+            const canvas: any = tmpCanvas.getCanvas();
+            const width: number = canvas.width;
+            const height: number = canvas.height;
+
+            // This magic number is an offset from the top image border so that
+            // elements above the staffline can be drawn correctly.
+            vsStaff.setY(vsStaff.y + 100);
+            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
+            vsStaff.setWidth(width);
+            measure.format();
+            vsStaff.setWidth(oldMeasureWidth);
+            measure.draw(ctx);
+
+            // imageData.data is a Uint8ClampedArray representing a one-dimensional array containing the data in the RGBA order
+            // RGBA is 32 bit word with 8 bits red, 8 bits green, 8 bits blue and 8 bit alpha. Alpha should be 0 for all background colors.
+            // Since we are only interested in black or white we can take 32bit words at once
+            const imageData: any = ctx.getImageData(0, 0, width, height);
+            const rgbaLength: number = 4;
+            const measureArrayLength: number = Math.max(Math.ceil(measure.PositionAndShape.Size.width * this.mRules.SamplingUnit), 1);
+            const tmpSkyLine: number[] = new Array(measureArrayLength);
+            const tmpBottomLine: number[] = new Array(measureArrayLength);
+            for (let x: number = 0; x < width; x++) {
+                // SkyLine
+                for (let y: number = 0; y < height; y++) {
+                    const yOffset: number = y * width * rgbaLength;
+                    const bufIndex: number = yOffset + x * rgbaLength;
+                    const alpha: number = imageData.data[bufIndex + 3];
+                    if (alpha > 0) {
+                        tmpSkyLine[x] = y;
+                        break;
+                    }
+                }
+                // BottomLine
+                for (let y: number = height; y > 0; y--) {
+                    const yOffset: number = y * width * rgbaLength;
+                    const bufIndex: number = yOffset + x * rgbaLength;
+                    const alpha: number = imageData.data[bufIndex + 3];
+                    if (alpha > 0) {
+                        tmpBottomLine[x] = y;
+                        break;
+                    }
+                }
+            }
+            this.mSkyLine.push(...tmpSkyLine);
+            this.mBottomLine.push(...tmpBottomLine);
+
+            // Set to true to only show the "mini canvases" and the corresponding skylines
+            const debugTmpCanvas: boolean = false;
+            if (debugTmpCanvas) {
+                tmpSkyLine.forEach((y, x) => this.drawPixel(new PointF2D(x, y), tmpCanvas));
+                tmpBottomLine.forEach((y, x) => this.drawPixel(new PointF2D(x, y), tmpCanvas, "blue"));
+                const img: any = canvas.toDataURL("image/png");
+                document.write('<img src="' + img + '"/>');
+            }
+            tmpCanvas.clear();
+        }
+        // Subsampling:
+        // The pixel width is bigger then the measure size in units. So we split the array into
+        // chunks with the size of MeasurePixelWidth/measureUnitWidth and reduce the value to its
+        // average
+        const arrayChunkSize: number = this.mSkyLine.length / arrayLength;
+
+        const subSampledSkyLine: number[] = [];
+        const subSampledBottomLine: number[] = [];
+        for (let chunkIndex: number = 0; chunkIndex < this.mSkyLine.length; chunkIndex += arrayChunkSize) {
+            let chunk: number[] = this.mSkyLine.slice(chunkIndex, chunkIndex + arrayChunkSize);
+            subSampledSkyLine.push(Math.min(...chunk));
+            chunk = this.mBottomLine.slice(chunkIndex, chunkIndex + arrayChunkSize);
+            subSampledBottomLine.push(Math.max(...chunk));
+        }
+
+        this.mSkyLine = subSampledSkyLine;
+        this.mBottomLine = subSampledBottomLine;
+        if (this.mSkyLine.length !== arrayLength) {
+            log.debug(`SkyLine calculation was not correct (${this.mSkyLine.length} instead of ${arrayLength})`);
+        }
+        if (this.mBottomLine.length !== arrayLength) {
+            log.debug(`BottomLine calculation was not correct (${this.mBottomLine.length} instead of ${arrayLength})`);
+        }
+        // Remap the values from 0 to +/- height in units
+        this.mSkyLine = this.mSkyLine.map(v => (v - Math.max(...this.mSkyLine)) / unitInPixels);
+        this.mBottomLine = this.mBottomLine.map(v => (v - Math.min(...this.mBottomLine)) / unitInPixels + this.mRules.StaffHeight);
+    }
+
+    /**
+     * Debugging drawing function that can draw single pixels
+     * @param coord Point to draw to
+     * @param backend the backend to be used
+     * @param color the color to be used, default is red
+     */
+    private drawPixel(coord: PointF2D, backend: CanvasVexFlowBackend, color: string = "#FF0000FF"): void {
+        const ctx: any = backend.getContext();
+        const oldStyle: string = ctx.fillStyle;
+        ctx.fillStyle = color;
+        ctx.fillRect( coord.x, coord.y, 2, 2 );
+        ctx.fillStyle = oldStyle;
+    }
+
+    /**
+     * This method updates the SkyLine for a given Wedge.
+     * @param  to update the SkyLine for
+     * @param start Start point of the wedge
+     * @param end End point of the wedge
+     */
+    public updateSkyLineWithWedge(start: PointF2D, end: PointF2D): void {
+        // FIXME: Refactor if wedges will be added. Current status is that vexflow will be used for this
+        let startIndex: number = Math.floor(start.x * this.SamplingUnit);
+        let endIndex: number = Math.ceil(end.x * this.SamplingUnit);
+
+        let slope: number = (end.y - start.y) / (end.x - start.x);
+
+        if (endIndex - startIndex <= 1) {
+            endIndex++;
+            slope = 0;
+        }
+
+        if (startIndex < 0) {
+            startIndex = 0;
+        }
+        if (startIndex >= this.BottomLine.length) {
+            startIndex = this.BottomLine.length - 1;
+        }
+        if (endIndex < 0) {
+            endIndex = 0;
+        }
+        if (endIndex >= this.BottomLine.length) {
+            endIndex = this.BottomLine.length;
+        }
+
+        this.SkyLine[startIndex] = start.y;
+        for (let i: number = startIndex + 1; i < Math.min(endIndex, this.SkyLine.length); i++) {
+            this.SkyLine[i] = this.SkyLine[i - 1] + slope / this.SamplingUnit;
+        }
+    }
+
+    /**
+     * This method updates the BottomLine for a given Wedge.
+     * @param  to update the bottomline for
+     * @param start Start point of the wedge
+     * @param end End point of the wedge
+     */
+    public updateBottomLineWithWedge(start: PointF2D, end: PointF2D): void {
+        // FIXME: Refactor if wedges will be added. Current status is that vexflow will be used for this
+        let startIndex: number = Math.floor(start.x * this.SamplingUnit);
+        let endIndex: number = Math.ceil(end.x * this.SamplingUnit);
+
+        let slope: number = (end.y - start.y) / (end.x - start.x);
+        if (endIndex - startIndex <= 1) {
+            endIndex++;
+            slope = 0;
+        }
+
+        if (startIndex < 0) {
+            startIndex = 0;
+        }
+        if (startIndex >= this.BottomLine.length) {
+            startIndex = this.BottomLine.length - 1;
+        }
+        if (endIndex < 0) {
+            endIndex = 0;
+        }
+        if (endIndex >= this.BottomLine.length) {
+            endIndex = this.BottomLine.length;
+        }
+
+        this.BottomLine[startIndex] = start.y;
+        for (let i: number = startIndex + 1; i < endIndex; i++) {
+            this.BottomLine[i] = this.BottomLine[i - 1] + slope / this.SamplingUnit;
+        }
+    }
+
+    /**
+     * This method updates the SkyLine for a given range with a given value
+     * @param  to update the SkyLine for
+     * @param start Start index of the range
+     * @param end End index of the range
+     * @param value ??
+     */
+    public updateSkyLineInRange(startIndex: number, endIndex: number, value: number): void {
+        this.updateInRange(this.mSkyLine, startIndex, endIndex, value);
+    }
+
+    /**
+     * This method updates the BottomLine for a given range with a given value
+     * @param  to update the BottomLine for
+     * @param start Start index of the range
+     * @param end End index of the range
+     * @param value ??
+     */
+    public updateBottomLineInRange(startIndex: number, endIndex: number, value: number): void {
+        this.updateInRange(this.BottomLine, startIndex, endIndex, value);
+    }
+
+    /**
+     * Resets a SkyLine in a range to its original value
+     * @param  to reset the SkyLine in
+     * @param startIndex Start index of the range
+     * @param endIndex End index of the range
+     */
+    public resetSkyLineInRange(startIndex: number, endIndex: number): void {
+        this.updateInRange(this.SkyLine, startIndex, endIndex);
+    }
+
+    /**
+     * Resets a bottom line in a range to its original value
+     * @param  to reset the bottomline in
+     * @param startIndex Start index of the range
+     * @param endIndex End index of the range
+     */
+    public resetBottomLineInRange(startIndex: number, endIndex: number): void {
+        this.updateInRange(this.BottomLine, startIndex, endIndex);
+    }
+
+    /**
+     * Update the whole skyline with a certain value
+     * @param value value to be set
+     */
+    public updateSkyLineWithValue(value: number): void {
+        this.SkyLine.forEach(sl => sl = value);
+    }
+
+    /**
+     * Update the whole bottomline with a certain value
+     * @param value value to be set
+     */
+    public updateBottomLineWithValue(value: number): void {
+        this.BottomLine.forEach(bl => bl = value);
+    }
+
+    public getLeftIndexForPointX(x: number, length: number): number {
+        const index: number = Math.floor(x * this.SamplingUnit);
+
+        if (index < 0) {
+            return 0;
+        }
+
+        if (index >= length) {
+            return length - 1;
+        }
+
+        return index;
+    }
+
+    public getRightIndexForPointX(x: number, length: number): number {
+        const index: number = Math.ceil(x * this.SamplingUnit);
+
+        if (index < 0) {
+            return 0;
+        }
+
+        if (index >= length) {
+            return length - 1;
+        }
+
+        return index;
+    }
+
+    /**
+     * This method updates the StaffLine Borders with the Sky- and BottomLines Min- and MaxValues.
+     */
+    public updateStaffLineBorders(): void {
+        this.mStaffLineParent.PositionAndShape.BorderTop = this.getSkyLineMin();
+        this.mStaffLineParent.PositionAndShape.BorderMarginTop = this.getSkyLineMin();
+        this.mStaffLineParent.PositionAndShape.BorderBottom = this.getBottomLineMax();
+        this.mStaffLineParent.PositionAndShape.BorderMarginBottom = this.getBottomLineMax();
+    }
+
+    /**
+     * This method finds the minimum value of the SkyLine.
+     * @param staffLine StaffLine to apply to
+     */
+    public getSkyLineMin(): number {
+        return Math.min(...this.SkyLine.filter(s => !isNaN(s)));
+    }
+
+    public getSkyLineMinAtPoint(point: number): number {
+        const index: number = Math.round(point * this.SamplingUnit);
+        return this.mSkyLine[index];
+    }
+
+    /**
+     * This method finds the SkyLine's minimum value within a given range.
+     * @param staffLine Staffline to apply to
+     * @param startIndex Starting index
+     * @param endIndex End index
+     */
+    public getSkyLineMinInRange(startIndex: number, endIndex: number): number {
+        return this.getMinInRange(this.SkyLine, startIndex, endIndex);
+    }
+
+    /**
+     * This method finds the maximum value of the BottomLine.
+     * @param staffLine Staffline to apply to
+     */
+    public getBottomLineMax(): number {
+        return Math.max(...this.BottomLine.filter(s => !isNaN(s)));
+    }
+
+    public getBottomLineMaxAtPoint(point: number): number {
+        const index: number = Math.round(point * this.SamplingUnit);
+        return this.mBottomLine[index];
+    }
+
+    /**
+     * This method finds the BottomLine's maximum value within a given range.
+     * @param staffLine Staffline to find the max value in
+     * @param startIndex Start index of the range
+     * @param endIndex End index of the range
+     */
+    public getBottomLineMaxInRange(startIndex: number, endIndex: number): number {
+        return this.getMaxInRange(this.BottomLine, startIndex, endIndex);
+    }
+
+
+    //#region Private methods
+
+    /**
+     * Update an array value inside a range
+     * @param array Array to fill in the new value
+     * @param startIndex start index to begin with (default: 0)
+     * @param endIndex end index of array (default: array length)
+     * @param value value to fill in (default: 0)
+     */
+    private updateInRange(array: number[], startIndex: number = 0, endIndex: number = array.length, value: number = 0): void {
+        startIndex = Math.floor(startIndex * this.SamplingUnit);
+        endIndex = Math.ceil(endIndex * this.SamplingUnit);
+
+        if (endIndex < startIndex) {
+            throw new Error("start index of line is greater then the end index");
+        }
+
+        if (startIndex < 0) {
+            startIndex = 0;
+        }
+
+        if (endIndex > array.length) {
+            endIndex = array.length;
+        }
+
+        for (let i: number = startIndex; i < endIndex; i++) {
+            array[i] = value;
+        }
+    }
+
+    /**
+     * Get all values of the selected line inside the given range
+     * @param skyBottomArray Skyline or bottom line
+     * @param startIndex start index
+     * @param endIndex end index
+     */
+    private getMinInRange(skyBottomArray: number[], startIndex: number, endIndex: number): number {
+        startIndex = Math.floor(startIndex * this.SamplingUnit);
+        endIndex = Math.ceil(endIndex * this.SamplingUnit);
+
+        if (skyBottomArray === undefined) {
+            // Highly questionable
+            return Number.MAX_VALUE;
+        }
+
+        if (startIndex < 0) {
+            startIndex = 0;
+        }
+        if (startIndex >= skyBottomArray.length) {
+            startIndex = skyBottomArray.length - 1;
+        }
+        if (endIndex < 0) {
+            endIndex = 0;
+        }
+        if (endIndex >= skyBottomArray.length) {
+            endIndex = skyBottomArray.length;
+        }
+
+        if (startIndex >= 0 && endIndex <= skyBottomArray.length) {
+            return Math.min(...skyBottomArray.slice(startIndex, endIndex));
+        }
+    }
+
+    /**
+     * Get the maximum value inside the given indices
+     * @param skyBottomArray Skyline or bottom line
+     * @param startIndex start index
+     * @param endIndex end index
+     */
+    private getMaxInRange(skyBottomArray: number[], startIndex: number, endIndex: number): number {
+        startIndex = Math.floor(startIndex * this.SamplingUnit);
+        endIndex = Math.ceil(endIndex * this.SamplingUnit);
+
+        if (skyBottomArray === undefined) {
+            // Highly questionable
+            return Number.MIN_VALUE;
+        }
+
+        if (startIndex < 0) {
+            startIndex = 0;
+        }
+        if (startIndex >= skyBottomArray.length) {
+            startIndex = skyBottomArray.length - 1;
+        }
+        if (endIndex < 0) {
+            endIndex = 0;
+        }
+        if (endIndex >= skyBottomArray.length) {
+            endIndex = skyBottomArray.length;
+        }
+
+        if (startIndex >= 0 && endIndex <= skyBottomArray.length) {
+            return Math.max(...skyBottomArray.slice(startIndex, endIndex));
+        }
+    }
+    // FIXME: What does this do here?
+    // private isStaffLineUpper(): boolean {
+    //     const instrument: Instrument = this.StaffLineParent.ParentStaff.ParentInstrument;
+
+    //     if (this.StaffLineParent.ParentStaff === instrument.Staves[0]) {
+    //         return true;
+    //     } else {
+    //         return false;
+    //     }
+    // }
+    // #endregion
+
+    //#region Getter Setter
+    /** Sampling units that are used to quantize the sky and bottom line  */
+    get SamplingUnit(): number {
+        return this.mRules.SamplingUnit;
+    }
+
+    /** Parent staffline where the skybottomline calculator is attached to */
+    get StaffLineParent(): StaffLine {
+        return this.mStaffLineParent;
+    }
+
+    /** Get the plain skyline array */
+    get SkyLine(): number[] {
+        return this.mSkyLine;
+    }
+
+    /** Get the plain bottomline array */
+    get BottomLine(): number[] {
+        return this.mBottomLine;
+    }
+    //#endregion
+}

+ 8 - 11
src/MusicalScore/Graphical/StaffLine.ts

@@ -9,6 +9,7 @@ import {MusicSystem} from "./MusicSystem";
 import {StaffLineActivitySymbol} from "./StaffLineActivitySymbol";
 import {PointF2D} from "../../Common/DataObjects/PointF2D";
 import {GraphicalLabel} from "./GraphicalLabel";
+import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
 
 /**
  * A StaffLine contains the [[Measure]]s in one line of the music sheet
@@ -19,8 +20,7 @@ export abstract class StaffLine extends GraphicalObject {
     protected staffLines: GraphicalLine[] = new Array(5);
     protected parentMusicSystem: MusicSystem;
     protected parentStaff: Staff;
-    protected skyLine: number[];
-    protected bottomLine: number[];
+    protected skyBottomLine: SkyBottomLineCalculator;
     protected lyricLines: GraphicalLine[] = [];
     protected lyricsDashes: GraphicalLabel[] = [];
 
@@ -29,6 +29,7 @@ export abstract class StaffLine extends GraphicalObject {
         this.parentMusicSystem = parentSystem;
         this.parentStaff = parentStaff;
         this.boundingBox = new BoundingBox(this, parentSystem.PositionAndShape);
+        this.skyBottomLine = new SkyBottomLineCalculator(this);
     }
 
     public get Measures(): GraphicalMeasure[] {
@@ -84,20 +85,16 @@ export abstract class StaffLine extends GraphicalObject {
         this.parentStaff = value;
     }
 
-    public get SkyLine(): number[] {
-        return this.skyLine;
+    public get SkyBottomLineCalculator(): SkyBottomLineCalculator {
+        return this.skyBottomLine;
     }
 
-    public set SkyLine(value: number[]) {
-        this.skyLine = value;
+    public get SkyLine(): number[] {
+        return this.skyBottomLine.SkyLine;
     }
 
     public get BottomLine(): number[] {
-        return this.bottomLine;
-    }
-
-    public set BottomLine(value: number[]) {
-        this.bottomLine = value;
+        return this.skyBottomLine.BottomLine;
     }
 
     public addActivitySymbolClickArea(): void {

+ 24 - 1
src/MusicalScore/Graphical/VexFlow/CanvasVexFlowBackend.ts

@@ -23,7 +23,20 @@ export class CanvasVexFlowBackend extends VexFlowBackend {
         this.renderer = new Vex.Flow.Renderer(this.canvas, this.getBackendType());
         this.ctx = <Vex.Flow.CanvasContext>this.renderer.getContext();
         this.canvasRenderingCtx = this.ctx.vexFlowCanvasContext;
+    }
 
+    /**
+     * Initialize a canvas without attaching it to a DOM node. Can be used to draw in background
+     * @param width Width of the canvas
+     * @param height Height of the canvas
+     */
+    public initializeHeadless(width: number = 300, height: number = 300): void {
+        this.canvas = document.createElement("canvas");
+        (this.canvas as any).width = width;
+        (this.canvas as any).height = height;
+        this.renderer = new Vex.Flow.Renderer(this.canvas, this.getBackendType());
+        this.ctx = <Vex.Flow.CanvasContext>this.renderer.getContext();
+        this.canvasRenderingCtx = this.ctx.vexFlowCanvasContext;
     }
 
     public getContext(): Vex.Flow.CanvasContext {
@@ -31,7 +44,7 @@ export class CanvasVexFlowBackend extends VexFlowBackend {
     }
 
     public clear(): void {
-        // Doesn't need to do anything
+        (<any>this.ctx).clearRect(0, 0, (<any>this.canvas).width, (<any>this.canvas).height);
     }
 
     public scale(k: number): void {
@@ -61,6 +74,16 @@ export class CanvasVexFlowBackend extends VexFlowBackend {
         this.canvasRenderingCtx.globalAlpha = 1;
     }
 
+    public renderLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF"): void {
+        const oldStyle: string | CanvasGradient | CanvasPattern = this.canvasRenderingCtx.strokeStyle;
+        this.canvasRenderingCtx.strokeStyle = color;
+        this.canvasRenderingCtx.beginPath();
+        this.canvasRenderingCtx.moveTo(start.x, start.y);
+        this.canvasRenderingCtx.lineTo(stop.x, stop.y);
+        this.canvasRenderingCtx.stroke();
+        this.canvasRenderingCtx.strokeStyle = oldStyle;
+    }
+
     private ctx: Vex.Flow.CanvasContext;
     private canvasRenderingCtx: CanvasRenderingContext2D;
 }

+ 12 - 0
src/MusicalScore/Graphical/VexFlow/SvgVexFlowBackend.ts

@@ -67,5 +67,17 @@ export class SvgVexFlowBackend extends VexFlowBackend {
         this.ctx.attributes["fill-opacity"] = 1;
     }
 
+    public renderLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF"): void {
+        this.ctx.save();
+        this.ctx.beginPath();
+        this.ctx.moveTo(start.x, start.y);
+        this.ctx.lineTo(stop.x, stop.y);
+        this.ctx.attributes.stroke = color;
+        this.ctx.lineWidth = 2;
+        this.ctx.attributes["stroke-linecap"] = "round";
+        this.ctx.stroke();
+        this.ctx.restore();
+    }
+
     private ctx: Vex.Flow.SVGContext;
 }

+ 2 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowBackend.ts

@@ -54,6 +54,8 @@ export abstract class VexFlowBackend {
    */
   public abstract renderRectangle(rectangle: RectangleF2D, styleId: number, alpha: number): void;
 
+  public abstract renderLine(start: PointF2D, stop: PointF2D, color: string): void;
+
   public abstract getBackendType(): number;
 
   protected renderer: Vex.Flow.Renderer;

+ 10 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowInstantaniousDynamicExpression.ts

@@ -0,0 +1,10 @@
+import { GraphicalInstantaniousDynamicExpression } from "../GraphicalInstantaniousDynamicExpression";
+import { InstantaniousDynamicExpression } from "../../VoiceData/Expressions/InstantaniousDynamicExpression";
+import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
+
+export class VexFlowInstantaniousDynamicExpression extends GraphicalInstantaniousDynamicExpression {
+
+    constructor(instantaniousDynamicExpression: InstantaniousDynamicExpression, staffEntry: GraphicalStaffEntry) {
+        super(instantaniousDynamicExpression);
+    }
+}

+ 8 - 24
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -3,10 +3,8 @@ import {VexFlowGraphicalSymbolFactory} from "./VexFlowGraphicalSymbolFactory";
 import {GraphicalMeasure} from "../GraphicalMeasure";
 import {StaffLine} from "../StaffLine";
 import {VoiceEntry} from "../../VoiceData/VoiceEntry";
-import {MusicSystem} from "../MusicSystem";
 import {GraphicalNote} from "../GraphicalNote";
 import {GraphicalStaffEntry} from "../GraphicalStaffEntry";
-import {GraphicalMusicPage} from "../GraphicalMusicPage";
 import {GraphicalTie} from "../GraphicalTie";
 import {Tie} from "../../VoiceData/Tie";
 import {SourceMeasure} from "../../VoiceData/SourceMeasure";
@@ -22,7 +20,6 @@ import {ArticulationEnum} from "../../VoiceData/VoiceEntry";
 import {Tuplet} from "../../VoiceData/Tuplet";
 import {VexFlowMeasure} from "./VexFlowMeasure";
 import {VexFlowTextMeasurer} from "./VexFlowTextMeasurer";
-
 import Vex = require("vexflow");
 import * as log from "loglevel";
 import {unitInPixels} from "./VexFlowMusicSheetDrawer";
@@ -134,7 +131,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
 
 
   protected updateStaffLineBorders(staffLine: StaffLine): void {
-    return;
+      staffLine.SkyBottomLineCalculator.updateStaffLineBorders();
   }
 
   protected graphicalMeasureCreatedCalculations(measure: GraphicalMeasure): void {
@@ -169,26 +166,13 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
    * furthermore the y positions of the systems themselves.
    */
   protected calculateSystemYLayout(): void {
-    for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-      const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-      if (!this.leadSheet) {
-        let globalY: number = this.rules.PageTopMargin + this.rules.TitleTopDistance + this.rules.SheetTitleHeight +
-          this.rules.TitleBottomDistance;
-        for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
-          const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
-          // calculate y positions of stafflines within system
-          let y: number = 0;
-          for (const line of musicSystem.StaffLines) {
-            line.PositionAndShape.RelativePosition.y = y;
-            y += 10;
-          }
-          // set y positions of systems using the previous system and a fixed distance.
-          musicSystem.PositionAndShape.BorderBottom = y + 0;
-          musicSystem.PositionAndShape.RelativePosition.x = this.rules.PageLeftMargin + this.rules.SystemLeftMargin;
-          musicSystem.PositionAndShape.RelativePosition.y = globalY;
-          globalY += y + 5;
-        }
-      }
+    for (const graphicalMusicPage of this.graphicalMusicSheet.MusicPages) {
+            for (const musicSystem of graphicalMusicPage.MusicSystems) {
+                this.optimizeDistanceBetweenStaffLines(musicSystem);
+            }
+
+            // set y positions of systems using the previous system and a fixed distance.
+            this.calculateMusicSystemsRelativePositions(graphicalMusicPage);
     }
   }
 

+ 98 - 13
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts

@@ -9,9 +9,11 @@ import {GraphicalObject} from "../GraphicalObject";
 import {GraphicalLayers} from "../DrawingEnums";
 import {GraphicalStaffEntry} from "../GraphicalStaffEntry";
 import {VexFlowBackend} from "./VexFlowBackend";
-import { VexFlowInstrumentBracket } from "./VexFlowInstrumentBracket";
-import { VexFlowInstrumentBrace } from "./VexFlowInstrumentBrace";
-import { GraphicalLyricEntry } from "../GraphicalLyricEntry";
+import {VexFlowInstrumentBracket} from "./VexFlowInstrumentBracket";
+import {VexFlowInstrumentBrace} from "./VexFlowInstrumentBrace";
+import {GraphicalLyricEntry} from "../GraphicalLyricEntry";
+import {StaffLine} from "../StaffLine";
+import {EngravingRules} from "../EngravingRules";
 
 /**
  * This is a global constant which denotes the height in pixels of the space between two lines of the stave
@@ -72,16 +74,6 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
             measure.PositionAndShape.AbsolutePosition.y * unitInPixels
         );
         measure.draw(this.backend.getContext());
-        for (const voiceID in measure.vfVoices) {
-            if (measure.vfVoices.hasOwnProperty(voiceID)) {
-                const tickables: Vex.Flow.Tickable[] = measure.vfVoices[voiceID].tickables;
-                for (const tick of tickables) {
-                    if ((<any>tick).getAttribute("type") === "StaveNote" && process.env.DEBUG) {
-                        tick.getBoundingBox().draw(this.backend.getContext());
-                    }
-                }
-            }
-        }
 
         // Draw the StaffEntries
         for (const staffEntry of measure.staffEntries) {
@@ -89,6 +81,99 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
         }
     }
 
+    // private drawPixel(coord: PointF2D): void {
+    //     coord = this.applyScreenTransformation(coord);
+    //     const ctx: any = this.backend.getContext();
+    //     const oldStyle: string = ctx.fillStyle;
+    //     ctx.fillStyle = "#00FF00FF";
+    //     ctx.fillRect( coord.x, coord.y, 2, 2 );
+    //     ctx.fillStyle = oldStyle;
+    // }
+
+    public drawLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF"): void {
+        start = this.applyScreenTransformation(start);
+        stop = this.applyScreenTransformation(stop);
+        this.backend.renderLine(start, stop, color);
+    }
+
+    protected drawSkyLine(staffline: StaffLine): void {
+        const startPosition: PointF2D = staffline.PositionAndShape.AbsolutePosition;
+        const width: number = staffline.PositionAndShape.Size.width;
+        this.drawSampledLine(staffline.SkyLine, startPosition, width);
+    }
+
+    protected drawBottomLine(staffline: StaffLine): void {
+        const startPosition: PointF2D = new PointF2D(staffline.PositionAndShape.AbsolutePosition.x,
+                                                     staffline.PositionAndShape.AbsolutePosition.y);
+        const width: number = staffline.PositionAndShape.Size.width;
+        this.drawSampledLine(staffline.BottomLine, startPosition, width, "#0000FFFF");
+    }
+
+    /**
+     * Draw a line with a width and start point in a chosen color (used for skyline/bottom line debugging) from
+     * a simple array
+     * @param line numeric array. 0 marks the base line. Direction given by sign. Dimensions in units
+     * @param startPosition Start position in units
+     * @param width Max line width in units
+     * @param color Color to paint in. Default is red
+     */
+    private drawSampledLine(line: number[], startPosition: PointF2D, width: number, color: string = "#FF0000FF"): void {
+        const indices: number[] = [];
+        let currentValue: number = 0;
+
+        for (let i: number = 0; i < line.length; i++) {
+            if (line[i] !== currentValue) {
+                indices.push(i);
+                currentValue = line[i];
+            }
+        }
+
+        const absolute: PointF2D = startPosition;
+        if (indices.length > 0) {
+            const samplingUnit: number = EngravingRules.Rules.SamplingUnit;
+
+            let horizontalStart: PointF2D = new PointF2D(absolute.x, absolute.y);
+            let horizontalEnd: PointF2D = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y);
+            this.drawLine(horizontalStart, horizontalEnd, color);
+
+            let verticalStart: PointF2D;
+            let verticalEnd: PointF2D;
+
+            if (line[0] >= 0) {
+                verticalStart = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y);
+                verticalEnd = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y + line[indices[0]]);
+                this.drawLine(verticalStart, verticalEnd, color);
+            }
+
+            for (let i: number = 1; i < indices.length; i++) {
+                horizontalStart = new PointF2D(indices[i - 1] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
+                horizontalEnd = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
+                this.drawLine(horizontalStart, horizontalEnd, color);
+
+                verticalStart = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
+                verticalEnd = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i]]);
+                this.drawLine(verticalStart, verticalEnd, color);
+            }
+
+            if (indices[indices.length - 1] < line.length) {
+                horizontalStart = new PointF2D(indices[indices.length - 1] / samplingUnit + absolute.x, absolute.y + line[indices[indices.length - 1]]);
+                horizontalEnd = new PointF2D(absolute.x + width, absolute.y + line[indices[indices.length - 1]]);
+                this.drawLine(horizontalStart, horizontalEnd, color);
+            } else {
+                horizontalStart = new PointF2D(indices[indices.length - 1] / samplingUnit + absolute.x, absolute.y);
+                horizontalEnd = new PointF2D(absolute.x + width, absolute.y);
+                this.drawLine(horizontalStart, horizontalEnd, color);
+            }
+        } else {
+            // Flat line
+            const start: PointF2D = new PointF2D(absolute.x, absolute.y);
+            const end: PointF2D = new PointF2D(absolute.x + width, absolute.y);
+            this.drawLine(start, end, color);
+        }
+    }
+
+
+
     private drawStaffEntry(staffEntry: GraphicalStaffEntry): void {
         // Draw ChordSymbol
         if (staffEntry.graphicalChordContainer !== undefined) {

+ 16 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSystem.ts

@@ -11,6 +11,7 @@ import {StaffLine} from "../StaffLine";
 import {EngravingRules} from "../EngravingRules";
 import { VexFlowInstrumentBracket } from "./VexFlowInstrumentBracket";
 import { VexFlowInstrumentBrace } from "./VexFlowInstrumentBrace";
+import { SkyBottomLineCalculator } from "../SkyBottomLineCalculator";
 
 export class VexFlowMusicSystem extends MusicSystem {
     constructor(parent: GraphicalMusicPage, id: number) {
@@ -26,6 +27,21 @@ export class VexFlowMusicSystem extends MusicSystem {
         this.boundingBox.BorderLeft = -width;
         this.boundingBox.BorderMarginLeft = -width;
         this.boundingBox.XBordersHaveBeenSet = true;
+
+        const topSkyBottomLineCalculator: SkyBottomLineCalculator = this.staffLines[0].SkyBottomLineCalculator;
+        const top: number = topSkyBottomLineCalculator.getSkyLineMin();
+        this.boundingBox.BorderTop = top;
+        this.boundingBox.BorderMarginTop = top;
+
+        const lastStaffLine: StaffLine = this.staffLines[this.staffLines.length - 1];
+        const bottomSkyBottomLineCalculator: SkyBottomLineCalculator = lastStaffLine.SkyBottomLineCalculator;
+        const bottom: number = bottomSkyBottomLineCalculator.getBottomLineMax()
+                    + lastStaffLine.PositionAndShape.RelativePosition.y;
+        this.boundingBox.BorderBottom = bottom;
+        this.boundingBox.BorderMarginBottom = bottom;
+
+        this.boundingBox.XBordersHaveBeenSet = true;
+        this.boundingBox.YBordersHaveBeenSet = true;
     }
 
     /**

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

@@ -44,5 +44,6 @@ export class VexFlowStaffEntry extends GraphicalStaffEntry {
         // sets the vexflow x positions back into the bounding boxes of the staff entries in the osmd object model.
         // The positions are needed for cursor placement and mouse/tap interactions
         this.PositionAndShape.RelativePosition.x = (tickablePosition - stave.getNoteStartX() + modifierOffset) / unitInPixels;
+        this.PositionAndShape.calculateBoundingBox();
     }
 }

+ 33 - 0
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -273,4 +273,37 @@ export class OpenSheetMusicDisplay {
         window.setTimeout(startCallback, 0);
         window.setTimeout(endCallback, 1);
     }
+
+    //#region GETTER / SETTER
+    public set DrawSkyLine(value: boolean) {
+        if (this.drawer) {
+            this.drawer.skyLineVisible = value;
+            this.render();
+        }
+    }
+
+    public get DrawSkyLine(): boolean {
+        return this.drawer.skyLineVisible;
+    }
+
+    public set DrawBottomLine(value: boolean) {
+        if (this.drawer) {
+            this.drawer.bottomLineVisible = value;
+            this.render();
+        }
+    }
+
+    public get DrawBottomLine(): boolean {
+        return this.drawer.bottomLineVisible;
+    }
+
+    public set DrawBoundingBox(value: string) {
+        this.drawer.drawableBoundingBoxElement = value;
+        this.render();
+    }
+
+    public get DrawBoundingBox(): string {
+        return this.drawer.drawableBoundingBoxElement;
+    }
+    //#endregion
 }

+ 9 - 9
test/Common/OSMD/OSMD_Test.ts

@@ -14,7 +14,7 @@ describe("OpenSheetMusicDisplay Main Export", () => {
     });
 
     it("container", (done: MochaDone) => {
-        const div: HTMLElement = document.createElement("div");
+        const div: HTMLElement = TestUtils.getDivElement(document);
         chai.expect(() => {
             return new OpenSheetMusicDisplay(div);
         }).to.not.throw(Error);
@@ -23,7 +23,7 @@ describe("OpenSheetMusicDisplay Main Export", () => {
 
     it("load MXL from string", (done: MochaDone) => {
         const mxl: string = TestUtils.getMXL("MozartTrio.mxl");
-        const div: HTMLElement = document.createElement("div");
+        const div: HTMLElement = TestUtils.getDivElement(document);
         const opensheetmusicdisplay: OpenSheetMusicDisplay = new OpenSheetMusicDisplay(div);
         opensheetmusicdisplay.load(mxl).then(
             (_: {}) => {
@@ -36,7 +36,7 @@ describe("OpenSheetMusicDisplay Main Export", () => {
 
     it("load invalid MXL from string", (done: MochaDone) => {
         const mxl: string = "\x50\x4b\x03\x04";
-        const div: HTMLElement = document.createElement("div");
+        const div: HTMLElement = TestUtils.getDivElement(document);
         const opensheetmusicdisplay: OpenSheetMusicDisplay = new OpenSheetMusicDisplay(div);
         opensheetmusicdisplay.load(mxl).then(
             (_: {}) => {
@@ -55,7 +55,7 @@ describe("OpenSheetMusicDisplay Main Export", () => {
     it("load XML string", (done: MochaDone) => {
         const score: Document = TestUtils.getScore("MuzioClementi_SonatinaOpus36No1_Part1.xml");
         const xml: string = new XMLSerializer().serializeToString(score);
-        const div: HTMLElement = document.createElement("div");
+        const div: HTMLElement = TestUtils.getDivElement(document);
         const opensheetmusicdisplay: OpenSheetMusicDisplay = new OpenSheetMusicDisplay(div);
         opensheetmusicdisplay.load(xml).then(
             (_: {}) => {
@@ -68,7 +68,7 @@ describe("OpenSheetMusicDisplay Main Export", () => {
 
     it("load XML Document", (done: MochaDone) => {
         const score: Document = TestUtils.getScore("MuzioClementi_SonatinaOpus36No1_Part1.xml");
-        const div: HTMLElement = document.createElement("div");
+        const div: HTMLElement = TestUtils.getDivElement(document);
         const opensheetmusicdisplay: OpenSheetMusicDisplay = new OpenSheetMusicDisplay(div);
         opensheetmusicdisplay.load(score).then(
             (_: {}) => {
@@ -81,7 +81,7 @@ describe("OpenSheetMusicDisplay Main Export", () => {
 
     it("load MXL Document by URL", (done: MochaDone) => {
         const url: string = "base/test/data/MozartTrio.mxl";
-        const div: HTMLElement = document.createElement("div");
+        const div: HTMLElement = TestUtils.getDivElement(document);
         const opensheetmusicdisplay: OpenSheetMusicDisplay = new OpenSheetMusicDisplay(div);
         opensheetmusicdisplay.load(url).then(
             (_: {}) => {
@@ -94,7 +94,7 @@ describe("OpenSheetMusicDisplay Main Export", () => {
 
     it("load MXL Document by invalid URL", (done: MochaDone) => {
         const url: string = "https://www.google.com";
-        const div: HTMLElement = document.createElement("div");
+        const div: HTMLElement = TestUtils.getDivElement(document);
         const opensheetmusicdisplay: OpenSheetMusicDisplay = new OpenSheetMusicDisplay(div);
         opensheetmusicdisplay.load(url).then(
             (_: {}) => {
@@ -112,7 +112,7 @@ describe("OpenSheetMusicDisplay Main Export", () => {
 
     it("load invalid XML string", (done: MochaDone) => {
         const xml: string = "<?xml";
-        const div: HTMLElement = document.createElement("div");
+        const div: HTMLElement = TestUtils.getDivElement(document);
         const opensheetmusicdisplay: OpenSheetMusicDisplay = new OpenSheetMusicDisplay(div);
         opensheetmusicdisplay.load(xml).then(
             (_: {}) => {
@@ -129,7 +129,7 @@ describe("OpenSheetMusicDisplay Main Export", () => {
     });
 
     it("render without loading", (done: MochaDone) => {
-        const div: HTMLElement = document.createElement("div");
+        const div: HTMLElement = TestUtils.getDivElement(document);
         const opensheetmusicdisplay: OpenSheetMusicDisplay = new OpenSheetMusicDisplay(div);
         chai.expect(() => {
             return opensheetmusicdisplay.render();

+ 7 - 0
test/Util/TestUtils.ts

@@ -15,6 +15,13 @@ export class TestUtils {
         return ((window as any).__raw__)[path];
     }
 
+    public static getDivElement(document: Document): HTMLElement {
+        const div: HTMLElement = document.createElement("div");
+        const body: HTMLElement = document.getElementsByTagName("body")[0];
+        body.appendChild(div);
+        return div;
+    }
+
     /**
      * Retrieve from a XML document the first element with name "score-partwise"
      * @param doc is the XML Document