Browse Source

feat(fingering): add fingerings to right, above, below, auto (#406)

resolves #350 

add OSMDOption and EngravingRule for fingeringPosition

bounding boxes for above and below fingerings wrong,
because FretFinger StringNumber use y-shift rendering without giving a bounding box

read placement from XML, refactor into method createFingerings

* fix(fingering): enable above/below staffline (use StringNumber again), add OSMDOption, EngravingRule

* demo sample: osmd function test all fingering more sensible
Simon 6 years ago
parent
commit
0e50f892ef

+ 4 - 2
demo/index.js

@@ -128,14 +128,16 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
         openSheetMusicDisplay = new OpenSheetMusicDisplay(canvas, {
         openSheetMusicDisplay = new OpenSheetMusicDisplay(canvas, {
             autoResize: true,
             autoResize: true,
             backend: backendSelect.value,
             backend: backendSelect.value,
-            drawingParameters: "default", // try compact (instead of default)
             disableCursor: false,
             disableCursor: false,
+            drawingParameters: "default", // try compact (instead of default)
             drawPartNames: true, // try false
             drawPartNames: true, // try false
             // drawTitle: false,
             // drawTitle: false,
             // drawSubtitle: false,
             // drawSubtitle: false,
             drawFingerings: true,
             drawFingerings: true,
+            fingeringPosition: "auto", // left is default. try right. experimental: auto, above, below.
+            // fingeringInsideStafflines: "true", // default: false. true draws fingerings directly above/below notes
 
 
-            // tupletsBracketed: true,
+            // tupletsBracketed: true, // creates brackets for all tuplets except triplets, even when not set by xml
             // tripletsBracketed: true,
             // tripletsBracketed: true,
             // tupletsRatioed: true, // unconventional; renders ratios for tuplets (3:2 instead of 3 for triplets)
             // tupletsRatioed: true, // unconventional; renders ratios for tuplets (3:2 instead of 3 for triplets)
         });
         });

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

@@ -231,6 +231,11 @@ declare namespace Vex {
             constructor(finger: string);
             constructor(finger: string);
         }
         }
 
 
+        export class StringNumber extends Modifier {
+            constructor(string: string);
+            setOffsetY(value: number);
+        }
+        
         export class Stroke extends Modifier {
         export class Stroke extends Modifier {
             constructor(type: number);
             constructor(type: number);
             public static Type: any; // unreliable values, use Arpeggio.ArpeggioType instead
             public static Type: any; // unreliable values, use Arpeggio.ArpeggioType instead

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

@@ -1,4 +1,5 @@
 import { EngravingRules } from "./EngravingRules";
 import { EngravingRules } from "./EngravingRules";
+import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
 
 
 export enum DrawingParametersEnum {
 export enum DrawingParametersEnum {
     allon = "allon",
     allon = "allon",
@@ -27,6 +28,7 @@ export class DrawingParameters {
     public drawComposer: boolean = true;
     public drawComposer: boolean = true;
     public drawCredits: boolean = true;
     public drawCredits: boolean = true;
     public drawPartNames: boolean = true;
     public drawPartNames: boolean = true;
+    public fingeringPosition: PlacementEnum = PlacementEnum.Left;
     /** Draw notes set to be invisible (print-object="no" in XML). */
     /** Draw notes set to be invisible (print-object="no" in XML). */
     public drawHiddenNotes: boolean = false;
     public drawHiddenNotes: boolean = false;
     public defaultColorNoteHead: string; // TODO not yet supported
     public defaultColorNoteHead: string; // TODO not yet supported
@@ -177,4 +179,13 @@ export class DrawingParameters {
         this.drawPartNames = value;
         this.drawPartNames = value;
         EngravingRules.Rules.RenderInstrumentNames = value;
         EngravingRules.Rules.RenderInstrumentNames = value;
     }
     }
+
+    public get FingeringPosition(): PlacementEnum {
+        return this.fingeringPosition;
+    }
+
+    public set FingeringPosition(value: PlacementEnum) {
+        this.fingeringPosition = value;
+        EngravingRules.Rules.FingeringPosition = value;
+    }
 }
 }

+ 18 - 1
src/MusicalScore/Graphical/EngravingRules.ts

@@ -1,7 +1,8 @@
-import {PagePlacementEnum} from "./GraphicalMusicPage";
+import { PagePlacementEnum } from "./GraphicalMusicPage";
 //import {MusicSymbol} from "./MusicSymbol";
 //import {MusicSymbol} from "./MusicSymbol";
 import * as log from "loglevel";
 import * as log from "loglevel";
 import { TextAlignmentEnum } from "../../Common/Enums/TextAlignment";
 import { TextAlignmentEnum } from "../../Common/Enums/TextAlignment";
+import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
 
 
 export class EngravingRules {
 export class EngravingRules {
     private static rules: EngravingRules;
     private static rules: EngravingRules;
@@ -166,6 +167,8 @@ export class EngravingRules {
     private renderLyricist: boolean;
     private renderLyricist: boolean;
     private renderInstrumentNames: boolean;
     private renderInstrumentNames: boolean;
     private renderFingerings: boolean;
     private renderFingerings: boolean;
+    private fingeringPosition: PlacementEnum;
+    private fingeringInsideStafflines: boolean;
 
 
     constructor() {
     constructor() {
         // global variables
         // global variables
@@ -351,6 +354,8 @@ export class EngravingRules {
         this.renderLyricist = true;
         this.renderLyricist = true;
         this.renderInstrumentNames = true;
         this.renderInstrumentNames = true;
         this.renderFingerings = true;
         this.renderFingerings = true;
+        this.fingeringPosition = PlacementEnum.Left; // easier to get bounding box, and safer for vertical layout
+        this.fingeringInsideStafflines = false;
 
 
         this.populateDictionaries();
         this.populateDictionaries();
         try {
         try {
@@ -1259,6 +1264,18 @@ export class EngravingRules {
     public set RenderFingerings(value: boolean) {
     public set RenderFingerings(value: boolean) {
         this.renderFingerings = value;
         this.renderFingerings = value;
     }
     }
+    public get FingeringPosition(): PlacementEnum {
+        return this.fingeringPosition;
+    }
+    public set FingeringPosition(value: PlacementEnum) {
+        this.fingeringPosition = value;
+    }
+    public get FingeringInsideStafflines(): boolean {
+        return this.fingeringInsideStafflines;
+    }
+    public set FingeringInsideStafflines(value: boolean) {
+        this.fingeringInsideStafflines = value;
+    }
 
 
     /**
     /**
      * This method maps NoteDurations to Distances and DistancesScalingFactors.
      * This method maps NoteDurations to Distances and DistancesScalingFactors.

+ 65 - 7
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -31,6 +31,7 @@ import {LinkedVoice} from "../../VoiceData/LinkedVoice";
 import {EngravingRules} from "../EngravingRules";
 import {EngravingRules} from "../EngravingRules";
 import {OrnamentContainer} from "../../VoiceData/OrnamentContainer";
 import {OrnamentContainer} from "../../VoiceData/OrnamentContainer";
 import {TechnicalInstruction} from "../../VoiceData/Instructions/TechnicalInstruction";
 import {TechnicalInstruction} from "../../VoiceData/Instructions/TechnicalInstruction";
+import {PlacementEnum} from "../../VoiceData/Expressions/AbstractExpression";
 import {ArpeggioType} from "../../VoiceData/Arpeggio";
 import {ArpeggioType} from "../../VoiceData/Arpeggio";
 import {VexFlowGraphicalNote} from "./VexFlowGraphicalNote";
 import {VexFlowGraphicalNote} from "./VexFlowGraphicalNote";
 
 
@@ -720,13 +721,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
 
 
                 // add fingering
                 // add fingering
                 if (voiceEntry.parentVoiceEntry && EngravingRules.Rules.RenderFingerings) {
                 if (voiceEntry.parentVoiceEntry && EngravingRules.Rules.RenderFingerings) {
-                    const technicalInstructions: TechnicalInstruction[] = voiceEntry.parentVoiceEntry.TechnicalInstructions;
-                    for (let i: number = 0; i < technicalInstructions.length; i++) {
-                        const technicalInstruction: TechnicalInstruction = technicalInstructions[i];
-                        const fretFinger: Vex.Flow.FretHandFinger = new Vex.Flow.FretHandFinger(technicalInstruction.value);
-                        fretFinger.setPosition(Vex.Flow.Modifier.Position.LEFT); // could be EngravingRule, see branch feature/fingeringsAboveEtc
-                        vexFlowVoiceEntry.vfStaveNote.addModifier(i, fretFinger);
-                    }
+                    this.createFingerings(voiceEntry);
                 }
                 }
 
 
                 // add Arpeggio
                 // add Arpeggio
@@ -807,6 +802,69 @@ export class VexFlowMeasure extends GraphicalMeasure {
         }
         }
     }
     }
 
 
+    private createFingerings(voiceEntry: GraphicalVoiceEntry): void {
+        const vexFlowVoiceEntry: VexFlowVoiceEntry = voiceEntry as VexFlowVoiceEntry;
+        const technicalInstructions: TechnicalInstruction[] = voiceEntry.parentVoiceEntry.TechnicalInstructions;
+        const fingeringsCount: number = technicalInstructions.length;
+        for (let i: number = 0; i < technicalInstructions.length; i++) {
+            const technicalInstruction: TechnicalInstruction = technicalInstructions[i];
+            let fingeringPosition: PlacementEnum = EngravingRules.Rules.FingeringPosition;
+            if (technicalInstruction.placement !== PlacementEnum.NotYetDefined) {
+                fingeringPosition = technicalInstruction.placement;
+            }
+            let modifierPosition: any; // Vex.Flow.Modifier.Position
+            switch (fingeringPosition) {
+                default:
+                case PlacementEnum.Left:
+                    modifierPosition = Vex.Flow.Modifier.Position.LEFT;
+                    break;
+                case PlacementEnum.Right:
+                    modifierPosition = Vex.Flow.Modifier.Position.RIGHT;
+                    break;
+                case PlacementEnum.Above:
+                    modifierPosition = Vex.Flow.Modifier.Position.ABOVE;
+                    break;
+                case PlacementEnum.Below:
+                    modifierPosition = Vex.Flow.Modifier.Position.BELOW;
+                    break;
+                case PlacementEnum.NotYetDefined: // automatic fingering placement, could be more complex/customizable
+                    const sourceStaff: Staff = voiceEntry.parentStaffEntry.sourceStaffEntry.ParentStaff;
+                    if (voiceEntry.notes.length > 1 || voiceEntry.parentStaffEntry.graphicalVoiceEntries.length > 1) {
+                        modifierPosition = Vex.Flow.Modifier.Position.LEFT;
+                    } else if (sourceStaff.idInMusicSheet === 0) {
+                        modifierPosition = Vex.Flow.Modifier.Position.ABOVE;
+                        fingeringPosition = PlacementEnum.Above;
+                    } else {
+                        modifierPosition = Vex.Flow.Modifier.Position.BELOW;
+                        fingeringPosition = PlacementEnum.Below;
+                    }
+            }
+
+            const fretFinger: Vex.Flow.FretHandFinger = new Vex.Flow.FretHandFinger(technicalInstruction.value);
+            fretFinger.setPosition(modifierPosition);
+            if (fingeringPosition === PlacementEnum.Above || fingeringPosition === PlacementEnum.Below) {
+                const offsetYSign: number = fingeringPosition === PlacementEnum.Above ? -1 : 1; // minus y is up
+                const ordering: number = fingeringPosition === PlacementEnum.Above ? i :
+                    technicalInstructions.length - 1 - i; // reverse order for fingerings below staff
+                if (EngravingRules.Rules.FingeringInsideStafflines && fingeringsCount > 1) { // y-shift for single fingering is ok
+                    // experimental, bounding boxes wrong for fretFinger above/below, better would be creating Labels
+                    // set y-shift. vexflow fretfinger simply places directly above/below note
+                    const perFingeringShift: number = fretFinger.getWidth() / 2;
+                    const shiftCount: number = fingeringsCount * 2.5;
+                    (<any>fretFinger).setOffsetY(offsetYSign * (ordering + shiftCount) * perFingeringShift);
+                } else if (!EngravingRules.Rules.FingeringInsideStafflines) { // use StringNumber for placement above/below stafflines
+                    const stringNumber: Vex.Flow.StringNumber = new Vex.Flow.StringNumber(technicalInstruction.value);
+                    (<any>stringNumber).radius = 0; // hack to remove the circle around the number
+                    stringNumber.setPosition(modifierPosition);
+                    stringNumber.setOffsetY(offsetYSign * ordering * stringNumber.getWidth() * 2 / 3);
+                    vexFlowVoiceEntry.vfStaveNote.addModifier(i, stringNumber);
+                    continue;
+                }
+            }
+            vexFlowVoiceEntry.vfStaveNote.addModifier(i, fretFinger);
+        }
+    }
+
     /**
     /**
      * Creates a line from 'top' to this measure, of type 'lineType'
      * Creates a line from 'top' to this measure, of type 'lineType'
      * @param top
      * @param top

+ 20 - 0
src/MusicalScore/ScoreIO/MusicSymbolModules/ArticulationReader.ts

@@ -125,6 +125,26 @@ export class ArticulationReader {
       const currentTechnicalInstruction: TechnicalInstruction = new TechnicalInstruction();
       const currentTechnicalInstruction: TechnicalInstruction = new TechnicalInstruction();
       currentTechnicalInstruction.type = TechnicalInstructionType.Fingering;
       currentTechnicalInstruction.type = TechnicalInstructionType.Fingering;
       currentTechnicalInstruction.value = nodeFingering.value;
       currentTechnicalInstruction.value = nodeFingering.value;
+      currentTechnicalInstruction.placement = PlacementEnum.NotYetDefined;
+      const placement: Attr = nodeFingering.attribute("placement");
+      if (placement !== undefined && placement !== null) {
+        switch (placement.value) {
+          case "above":
+            currentTechnicalInstruction.placement = PlacementEnum.Above;
+            break;
+          case "below":
+            currentTechnicalInstruction.placement = PlacementEnum.Below;
+            break;
+          case "left": // not valid in MusicXML 3.1
+            currentTechnicalInstruction.placement = PlacementEnum.Left;
+            break;
+          case "right": // not valid in MusicXML 3.1
+            currentTechnicalInstruction.placement = PlacementEnum.Right;
+            break;
+          default:
+            currentTechnicalInstruction.placement = PlacementEnum.NotYetDefined;
+        }
+      }
       currentVoiceEntry.TechnicalInstructions.push(currentTechnicalInstruction);
       currentVoiceEntry.TechnicalInstructions.push(currentTechnicalInstruction);
     }
     }
   }
   }

+ 19 - 1
src/MusicalScore/VoiceData/Expressions/AbstractExpression.ts

@@ -11,10 +11,28 @@ export class AbstractExpression {
         }
         }
         return false;
         return false;
     }
     }
+
+    public static PlacementEnumFromString(placementString: string): PlacementEnum {
+        switch (placementString.toLowerCase()) {
+            case "above":
+                return PlacementEnum.Above;
+            case "below":
+                return PlacementEnum.Below;
+            case "left":
+                return PlacementEnum.Left;
+            case "right":
+                return PlacementEnum.Right;
+            case "auto":
+            default:
+                return PlacementEnum.NotYetDefined;
+        }
+    }
 }
 }
 
 
 export enum PlacementEnum {
 export enum PlacementEnum {
     Above = 0,
     Above = 0,
     Below = 1,
     Below = 1,
-    NotYetDefined = 2
+    Left = 2,
+    Right = 3,
+    NotYetDefined = 4
 }
 }

+ 3 - 0
src/MusicalScore/VoiceData/Instructions/TechnicalInstruction.ts

@@ -1,7 +1,10 @@
+import { PlacementEnum } from "../Expressions/AbstractExpression";
+
 export enum TechnicalInstructionType {
 export enum TechnicalInstructionType {
     Fingering
     Fingering
 }
 }
 export class TechnicalInstruction {
 export class TechnicalInstruction {
     public type: TechnicalInstructionType;
     public type: TechnicalInstructionType;
     public value: string;
     public value: string;
+    public placement: PlacementEnum;
 }
 }

+ 12 - 6
src/OpenSheetMusicDisplay/OSMDOptions.ts

@@ -14,12 +14,6 @@ export interface IOSMDOptions {
     disableCursor?: boolean;
     disableCursor?: boolean;
     /** Broad Parameters like compact or preview mode. */
     /** Broad Parameters like compact or preview mode. */
     drawingParameters?: string | DrawingParametersEnum;
     drawingParameters?: string | DrawingParametersEnum;
-    /** Whether to draw hidden/invisible notes (print-object="no" in XML). Default false. Not yet supported. */ // TODO
-    drawHiddenNotes?: boolean;
-    /** Default color for a note head (without stem). Default black. Not yet supported. */ // TODO
-    defaultColorNoteHead?: string;
-    /** Default color for a note stem. Default black. Not yet supported. */ // TODO
-    defaultColorStem?: string;
     /** Whether to draw the title of the piece. If false, disables drawing Subtitle as well. */
     /** Whether to draw the title of the piece. If false, disables drawing Subtitle as well. */
     drawTitle?: boolean;
     drawTitle?: boolean;
     /** Whether to draw the subtitle of the piece. If true, enables drawing Title as well. */
     /** Whether to draw the subtitle of the piece. If true, enables drawing Title as well. */
@@ -32,6 +26,12 @@ export interface IOSMDOptions {
     drawPartNames?: boolean;
     drawPartNames?: boolean;
     /** Whether to draw fingerings (only left to the note for now). Default true. */
     /** Whether to draw fingerings (only left to the note for now). Default true. */
     drawFingerings?: boolean;
     drawFingerings?: boolean;
+    /** Where to draw fingerings (left, right, above, below, auto).
+     * Default left. Auto, above, below experimental (potential collisions because bounding box not correct)
+     */
+    fingeringPosition?: string;
+    /** For above/below fingerings, whether to draw them directly above/below notes (default), or above/below staffline. */
+    fingeringInsideStafflines?: boolean;
     /** Whether tuplets are labeled with ratio (e.g. 5:2 instead of 5 for quintuplets). Default false. */
     /** Whether tuplets are labeled with ratio (e.g. 5:2 instead of 5 for quintuplets). Default false. */
     tupletsRatioed?: boolean;
     tupletsRatioed?: boolean;
     /** Whether all tuplets should be bracketed (e.g. |--5--| instead of 5). Default false.
     /** Whether all tuplets should be bracketed (e.g. |--5--| instead of 5). Default false.
@@ -43,6 +43,12 @@ export interface IOSMDOptions {
      * (Bracketing all triplets can be cluttering)
      * (Bracketing all triplets can be cluttering)
      */
      */
     tripletsBracketed?: boolean;
     tripletsBracketed?: boolean;
+    /** Whether to draw hidden/invisible notes (print-object="no" in XML). Default false. Not yet supported. */ // TODO
+    drawHiddenNotes?: boolean;
+    /** Default color for a note head (without stem). Default black. Not yet supported. */ // TODO
+    defaultColorNoteHead?: string;
+    /** Default color for a note stem. Default black. Not yet supported. */ // TODO
+    defaultColorStem?: string;
 }
 }
 
 
 /** Handles [[IOSMDOptions]], e.g. returning default options with OSMDOptionsStandard() */
 /** Handles [[IOSMDOptions]], e.g. returning default options with OSMDOptionsStandard() */

+ 7 - 0
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -16,6 +16,7 @@ import * as log from "loglevel";
 import {DrawingParametersEnum, DrawingParameters} from "../MusicalScore/Graphical/DrawingParameters";
 import {DrawingParametersEnum, DrawingParameters} from "../MusicalScore/Graphical/DrawingParameters";
 import {IOSMDOptions, OSMDOptions} from "./OSMDOptions";
 import {IOSMDOptions, OSMDOptions} from "./OSMDOptions";
 import {EngravingRules} from "../MusicalScore/Graphical/EngravingRules";
 import {EngravingRules} from "../MusicalScore/Graphical/EngravingRules";
+import {AbstractExpression} from "../MusicalScore/VoiceData/Expressions/AbstractExpression";
 
 
 /**
 /**
  * The main class and control point of OpenSheetMusicDisplay.<br>
  * The main class and control point of OpenSheetMusicDisplay.<br>
@@ -360,6 +361,12 @@ export class OpenSheetMusicDisplay {
         if (options.drawFingerings === false) {
         if (options.drawFingerings === false) {
             EngravingRules.Rules.RenderFingerings = false;
             EngravingRules.Rules.RenderFingerings = false;
         }
         }
+        if (options.fingeringPosition !== undefined) {
+            EngravingRules.Rules.FingeringPosition = AbstractExpression.PlacementEnumFromString(options.fingeringPosition);
+        }
+        if (options.fingeringInsideStafflines !== undefined) {
+            EngravingRules.Rules.FingeringInsideStafflines = options.fingeringInsideStafflines;
+        }
         if (options.defaultColorNoteHead) {
         if (options.defaultColorNoteHead) {
             this.drawingParameters.defaultColorNoteHead = options.defaultColorNoteHead;
             this.drawingParameters.defaultColorNoteHead = options.defaultColorNoteHead;
         }
         }

+ 7 - 7
test/data/OSMD_function_test_all.xml

@@ -772,7 +772,7 @@
         <stem>up</stem>
         <stem>up</stem>
         <notations>
         <notations>
           <technical>
           <technical>
-            <fingering>1</fingering>
+            <fingering>2</fingering>
             </technical>
             </technical>
           </notations>
           </notations>
         <lyric number="1" default-x="6.22" default-y="-80.00">
         <lyric number="1" default-x="6.22" default-y="-80.00">
@@ -792,7 +792,7 @@
         <stem>up</stem>
         <stem>up</stem>
         <notations>
         <notations>
           <technical>
           <technical>
-            <fingering>2</fingering>
+            <fingering>3</fingering>
             </technical>
             </technical>
           </notations>
           </notations>
         </note>
         </note>
@@ -808,7 +808,7 @@
         <stem>up</stem>
         <stem>up</stem>
         <notations>
         <notations>
           <technical>
           <technical>
-            <fingering>3</fingering>
+            <fingering>5</fingering>
             </technical>
             </technical>
           </notations>
           </notations>
         </note>
         </note>
@@ -840,7 +840,7 @@
         <beam number="1">end</beam>
         <beam number="1">end</beam>
         <notations>
         <notations>
           <technical>
           <technical>
-            <fingering>2</fingering>
+            <fingering>3</fingering>
             </technical>
             </technical>
           </notations>
           </notations>
         </note>
         </note>
@@ -859,7 +859,7 @@
         <beam number="2">begin</beam>
         <beam number="2">begin</beam>
         <notations>
         <notations>
           <technical>
           <technical>
-            <fingering>3</fingering>
+            <fingering>2</fingering>
             </technical>
             </technical>
           </notations>
           </notations>
         </note>
         </note>
@@ -876,7 +876,7 @@
         <beam number="2">continue</beam>
         <beam number="2">continue</beam>
         <notations>
         <notations>
           <technical>
           <technical>
-            <fingering>4</fingering>
+            <fingering>3</fingering>
             </technical>
             </technical>
           </notations>
           </notations>
         </note>
         </note>
@@ -893,7 +893,7 @@
         <beam number="2">continue</beam>
         <beam number="2">continue</beam>
         <notations>
         <notations>
           <technical>
           <technical>
-            <fingering>5</fingering>
+            <fingering>4</fingering>
             </technical>
             </technical>
           </notations>
           </notations>
         </note>
         </note>