Browse Source

feat(stemDirection): use xml stem direction. (optionally)

read xml stem element, add xmlWantedDirection to Note, VoiceEntry
add OSMDOption, EngravingRule setWantedStemDirectionByXml

add Double stem direction type (unsupported)

resolves #415
sschmidTU 6 years ago
parent
commit
6373d58921

+ 1 - 0
demo/index.js

@@ -137,6 +137,7 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
             drawFingerings: true,
             fingeringPosition: "auto", // left is default. try right. experimental: auto, above, below.
             // fingeringInsideStafflines: "true", // default: false. true draws fingerings directly above/below notes
+            setWantedStemDirectionByXml: true, // try false, which was previously the default behavior
 
             // tupletsBracketed: true, // creates brackets for all tuplets except triplets, even when not set by xml
             // tripletsBracketed: true,

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

@@ -60,6 +60,7 @@ export class EngravingRules {
     private stemMaxLength: number;
     private beamSlopeMaxAngle: number;
     private stemMinAllowedDistanceBetweenNoteHeadAndBeamLine: number;
+    private setWantedStemDirectionByXml: boolean;
     private graceNoteScalingFactor: number;
     private graceNoteXOffset: number;
     private wedgeOpeningLength: number;
@@ -241,6 +242,7 @@ export class EngravingRules {
         this.stemMaxLength = 4.5;
         this.beamSlopeMaxAngle = 10.0;
         this.stemMinAllowedDistanceBetweenNoteHeadAndBeamLine = 1.0;
+        this.setWantedStemDirectionByXml = true;
 
         // GraceNote Variables
         this.graceNoteScalingFactor = 0.6;
@@ -692,6 +694,12 @@ export class EngravingRules {
     public set StemMinAllowedDistanceBetweenNoteHeadAndBeamLine(value: number) {
         this.stemMinAllowedDistanceBetweenNoteHeadAndBeamLine = value;
     }
+    public get SetWantedStemDirectionByXml(): boolean {
+        return this.setWantedStemDirectionByXml;
+    }
+    public set SetWantedStemDirectionByXml(value: boolean) {
+        this.setWantedStemDirectionByXml = value;
+    }
     public get GraceNoteScalingFactor(): number {
         return this.graceNoteScalingFactor;
     }

+ 7 - 1
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -1486,7 +1486,13 @@ export abstract class MusicSheetCalculator {
                                openTuplets: Tuplet[], openBeams: Beam[],
                                octaveShiftValue: OctaveEnum, linkedNotes: Note[] = undefined,
                                sourceStaffEntry: SourceStaffEntry = undefined): OctaveEnum {
-        this.calculateStemDirectionFromVoices(voiceEntry);
+        if (voiceEntry.WantedStemDirectionXml !== StemDirectionType.Undefined &&
+            EngravingRules.Rules.SetWantedStemDirectionByXml &&
+            voiceEntry.WantedStemDirectionXml !== undefined) {
+                voiceEntry.WantedStemDirection = voiceEntry.WantedStemDirectionXml;
+        } else {
+            this.calculateStemDirectionFromVoices(voiceEntry);
+        }
         const gve: GraphicalVoiceEntry = graphicalStaffEntry.findOrCreateGraphicalVoiceEntry(voiceEntry);
         gve.octaveShiftValue = octaveShiftValue;
         for (let idx: number = 0, len: number = voiceEntry.Notes.length; idx < len; ++idx) {

+ 26 - 3
src/MusicalScore/ScoreIO/InstrumentReader.ts

@@ -21,8 +21,9 @@ import * as log from "loglevel";
 import {MidiInstrument} from "../VoiceData/Instructions/ClefInstruction";
 import {ChordSymbolReader} from "./MusicSymbolModules/ChordSymbolReader";
 import {ExpressionReader} from "./MusicSymbolModules/ExpressionReader";
-import { RepetitionInstructionReader } from "./MusicSymbolModules/RepetitionInstructionReader";
-import { SlurReader } from "./MusicSymbolModules/SlurReader";
+import {RepetitionInstructionReader} from "./MusicSymbolModules/RepetitionInstructionReader";
+import {SlurReader} from "./MusicSymbolModules/SlurReader";
+import {StemDirectionType} from "../VoiceData/VoiceEntry";
 //import Dictionary from "typescript-collections/dist/lib/Dictionary";
 
 // FIXME: The following classes are missing
@@ -223,6 +224,28 @@ export class InstrumentReader {
             }
           }
 
+          // check stem element
+          let stemDirectionXml: StemDirectionType = StemDirectionType.Undefined;
+          const stemNode: IXmlElement = xmlNode.element("stem");
+          if (stemNode !== undefined) {
+            switch (stemNode.value) {
+              case "down":
+                stemDirectionXml = StemDirectionType.Down;
+                break;
+              case "up":
+                stemDirectionXml = StemDirectionType.Up;
+                break;
+              case "double":
+                stemDirectionXml = StemDirectionType.Double;
+                break;
+              case "none":
+                stemDirectionXml = StemDirectionType.None;
+                break;
+              default:
+                stemDirectionXml = StemDirectionType.Undefined;
+            }
+          }
+
           let musicTimestamp: Fraction = currentFraction.clone();
           if (isChord) {
             musicTimestamp = previousFraction.clone();
@@ -276,7 +299,7 @@ export class InstrumentReader {
             xmlNode, noteDuration, restNote,
             this.currentStaffEntry, this.currentMeasure,
             measureStartAbsoluteTimestamp,
-            this.maxTieNoteFraction, isChord, guitarPro, printObject, isCueNote
+            this.maxTieNoteFraction, isChord, guitarPro, printObject, isCueNote, stemDirectionXml
           );
 
           const notationsNode: IXmlElement = xmlNode.element("notations");

+ 6 - 4
src/MusicalScore/ScoreIO/VoiceGenerator.ts

@@ -2,7 +2,7 @@ import { Instrument } from "../Instrument";
 import { LinkedVoice } from "../VoiceData/LinkedVoice";
 import { Voice } from "../VoiceData/Voice";
 import { MusicSheet } from "../MusicSheet";
-import { VoiceEntry } from "../VoiceData/VoiceEntry";
+import { VoiceEntry, StemDirectionType } from "../VoiceData/VoiceEntry";
 import { Note } from "../VoiceData/Note";
 import { SourceMeasure } from "../VoiceData/SourceMeasure";
 import { SourceStaffEntry } from "../VoiceData/SourceStaffEntry";
@@ -106,14 +106,14 @@ export class VoiceGenerator {
   public read(noteNode: IXmlElement, noteDuration: Fraction, restNote: boolean,
               parentStaffEntry: SourceStaffEntry, parentMeasure: SourceMeasure,
               measureStartAbsoluteTimestamp: Fraction, maxTieNoteFraction: Fraction, chord: boolean, guitarPro: boolean,
-              printObject: boolean = true, isCueNote: boolean = false): Note {
+              printObject: boolean, isCueNote: boolean, stemDirectionXml: StemDirectionType): Note {
     this.currentStaffEntry = parentStaffEntry;
     this.currentMeasure = parentMeasure;
     //log.debug("read called:", restNote);
     try {
       this.currentNote = restNote
         ? this.addRestNote(noteDuration, printObject, isCueNote)
-        : this.addSingleNote(noteNode, noteDuration, chord, guitarPro, printObject, isCueNote);
+        : this.addSingleNote(noteNode, noteDuration, chord, guitarPro, printObject, isCueNote, stemDirectionXml);
       // read lyrics
       const lyricElements: IXmlElement[] = noteNode.elements("lyric");
       if (this.lyricsReader !== undefined && lyricElements !== undefined) {
@@ -321,7 +321,7 @@ export class VoiceGenerator {
    * @returns {Note}
    */
   private addSingleNote(node: IXmlElement, noteDuration: Fraction, chord: boolean, guitarPro: boolean,
-                        printObject: boolean = true, isCueNote: boolean = false): Note {
+                        printObject: boolean, isCueNote: boolean, stemDirectionXml: StemDirectionType): Note {
     //log.debug("addSingleNote called");
     let noteAlter: number = 0;
     let noteAccidental: AccidentalEnum = AccidentalEnum.NONE;
@@ -416,11 +416,13 @@ export class VoiceGenerator {
     const note: Note = new Note(this.currentVoiceEntry, this.currentStaffEntry, noteLength, pitch);
     note.PrintObject = printObject;
     note.IsCueNote = isCueNote;
+    note.StemDirectionXml = stemDirectionXml;
     note.PlaybackInstrumentId = playbackInstrumentId;
     if (noteHeadShapeXml !== undefined && noteHeadShapeXml !== "normal") {
       note.NoteHead = new NoteHead(note, noteHeadShapeXml, noteHeadFilledXml);
     } // if normal, leave note head undefined to save processing/runtime
     this.currentVoiceEntry.Notes.push(note);
+    this.currentVoiceEntry.WantedStemDirectionXml = stemDirectionXml;
     if (node.elements("beam") && !chord) {
       this.createBeam(node, note);
     }

+ 9 - 1
src/MusicalScore/VoiceData/Note.ts

@@ -1,4 +1,4 @@
-import {VoiceEntry} from "./VoiceEntry";
+import {VoiceEntry, StemDirectionType} from "./VoiceEntry";
 import {SourceStaffEntry} from "./SourceStaffEntry";
 import {Fraction} from "../../Common/DataObjects/Fraction";
 import {Pitch} from "../../Common/DataObjects/Pitch";
@@ -52,6 +52,8 @@ export class Note {
     private arpeggio: Arpeggio;
     /** States whether this is a cue note (Stichnote) (smaller size). */
     private isCueNote: boolean;
+    /** The stem direction asked for in XML. Not necessarily final or wanted stem direction. */
+    private stemDirectionXml: StemDirectionType;
 
     public get ParentVoiceEntry(): VoiceEntry {
         return this.voiceEntry;
@@ -128,6 +130,12 @@ export class Note {
     public set IsCueNote(value: boolean) {
         this.isCueNote = value;
     }
+    public get StemDirectionXml(): StemDirectionType {
+        return this.stemDirectionXml;
+    }
+    public set StemDirectionXml(value: StemDirectionType) {
+        this.stemDirectionXml = value;
+    }
 
     public isRest(): boolean {
         return this.Pitch === undefined;

+ 10 - 1
src/MusicalScore/VoiceData/VoiceEntry.ts

@@ -51,6 +51,8 @@ export class VoiceEntry {
     private arpeggio: Arpeggio;
     private ornamentContainer: OrnamentContainer;
     private wantedStemDirection: StemDirectionType = StemDirectionType.Undefined;
+    /** Stem direction specified in the xml stem element. */
+    private wantedStemDirectionXml: StemDirectionType = StemDirectionType.Undefined;
     private stemDirection: StemDirectionType = StemDirectionType.Undefined;
 
     public get ParentSourceStaffEntry(): SourceStaffEntry {
@@ -122,6 +124,12 @@ export class VoiceEntry {
     public get WantedStemDirection(): StemDirectionType {
         return this.wantedStemDirection;
     }
+    public set WantedStemDirectionXml(value: StemDirectionType) {
+        this.wantedStemDirectionXml = value;
+    }
+    public get WantedStemDirectionXml(): StemDirectionType {
+        return this.wantedStemDirectionXml;
+    }
     // StemDirection holds the actual value of the stem
     public set StemDirection(value: StemDirectionType) {
         this.stemDirection = value;
@@ -383,5 +391,6 @@ export enum StemDirectionType {
     Undefined = -1,
     Up = 0,
     Down = 1,
-    None = 2
+    None = 2,
+    Double = 3
 }

+ 2 - 0
src/OpenSheetMusicDisplay/OSMDOptions.ts

@@ -32,6 +32,8 @@ export interface IOSMDOptions {
     fingeringPosition?: string;
     /** For above/below fingerings, whether to draw them directly above/below notes (default), or above/below staffline. */
     fingeringInsideStafflines?: boolean;
+    /** Whether to set the wanted stem direction by xml (default) or automatically. */
+    setWantedStemDirectionByXml?: boolean;
     /** Whether tuplets are labeled with ratio (e.g. 5:2 instead of 5 for quintuplets). Default false. */
     tupletsRatioed?: boolean;
     /** Whether all tuplets should be bracketed (e.g. |--5--| instead of 5). Default false.

+ 3 - 0
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -374,6 +374,9 @@ export class OpenSheetMusicDisplay {
         if (options.fingeringInsideStafflines !== undefined) {
             EngravingRules.Rules.FingeringInsideStafflines = options.fingeringInsideStafflines;
         }
+        if (options.setWantedStemDirectionByXml !== undefined) {
+            EngravingRules.Rules.SetWantedStemDirectionByXml = options.setWantedStemDirectionByXml;
+        }
         if (options.defaultColorNoteHead) {
             this.drawingParameters.defaultColorNoteHead = options.defaultColorNoteHead;
         }