浏览代码

merge osmd-public: fix fingerings collisions above/below, improve fingering performance, fix slash-flat accidentals

* osmd-public/develop:
fix(Fingerings): Fix Fingerings collisions above/below notes (osmd-public 1081), implement as Labels with correct bboxes
fix(Slash-Flat Accidentals): Fix quarter flats shown after slash-flat accidentals (1075)
Readme.md: Limitations: Add hint to SVG manipulation like instant note re-coloring
fix rests not positioned above when voice === 5 (bass top voice, 621)
sschmid 3 年之前
父节点
当前提交
a200aa6f4f

+ 5 - 1
demo/index.js

@@ -463,7 +463,8 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
             // drawTitle: false,
             // drawSubtitle: false,
             drawFingerings: true,
-            fingeringPosition: "left", // left is default. try right. experimental: auto, above, below.
+            //fingeringPosition: "left", // Above/Below is default. try left or right. experimental: above, below.
+            //fingeringPositionFromXML: false, // do this if you want them always left, for example.
             // fingeringInsideStafflines: "true", // default: false. true draws fingerings directly above/below notes
             setWantedStemDirectionByXml: true, // try false, which was previously the default behavior
             // drawUpToMeasureNumber: 3, // draws only up to measure 3, meaning it draws measure 1 to 3 of the piece.
@@ -495,6 +496,9 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
             // tripletsBracketed: true,
             // tupletsRatioed: true, // unconventional; renders ratios for tuplets (3:2 instead of 3 for triplets)
         });
+        //openSheetMusicDisplay.DrawSkyLine = true;
+        //openSheetMusicDisplay.DrawBottomLine = true;
+        //openSheetMusicDisplay.setDrawBoundingBox("GraphicalLabel", false);
         openSheetMusicDisplay.setLogLevel('info'); // set this to 'debug' if you want to see more detailed control flow information in console
         document.body.appendChild(canvas);
 

+ 7 - 1
src/Common/DataObjects/Pitch.ts

@@ -45,6 +45,7 @@ export class Pitch {
     private octave: number;
     private fundamentalNote: NoteEnum;
     private accidental: AccidentalEnum = AccidentalEnum.NONE;
+    private accidentalXml: string;
     private frequency: number;
     private halfTone: number;
 
@@ -187,10 +188,11 @@ export class Pitch {
         return fundamentalNote;
     }
 
-    constructor(fundamentalNote: NoteEnum, octave: number, accidental: AccidentalEnum) {
+    constructor(fundamentalNote: NoteEnum, octave: number, accidental: AccidentalEnum, accidentalXml: string = undefined) {
         this.fundamentalNote = fundamentalNote;
         this.octave = octave;
         this.accidental = accidental;
+        this.accidentalXml = accidentalXml;
         this.halfTone = <number>(fundamentalNote) + (octave + Pitch.octXmlDiff) * 12 +
             Pitch.HalfTonesFromAccidental(accidental);
         this.frequency = Pitch.calcFrequency(this);
@@ -338,6 +340,10 @@ export class Pitch {
         return this.accidental;
     }
 
+    public get AccidentalXml(): string {
+        return this.accidentalXml;
+    }
+
     public get Frequency(): number {
         return this.frequency;
     }

+ 10 - 0
src/MusicalScore/Graphical/AccidentalCalculator.ts

@@ -88,6 +88,9 @@ export class AccidentalCalculator {
                         pitch = new Pitch(pitch.FundamentalNote, pitch.Octave, AccidentalEnum.NATURAL);
                     }
                 }
+                if (this.isAlterAmbiguousAccidental(pitch.Accidental) && ! pitch.AccidentalXml) {
+                    return; // only display accidental if it was given as an accidental in the XML
+                }
                 MusicSheetCalculator.symbolFactory.addGraphicalAccidental(graphicalNote, pitch);
             }
         } else { // pitchkey not in measure dict:
@@ -96,6 +99,9 @@ export class AccidentalCalculator {
                     this.currentAlterationsComparedToKeyInstructionList.push(pitchKey);
                 }
                 this.currentInMeasureNoteAlterationsDict.setValue(pitchKey, pitch.AccidentalHalfTones);
+                if (this.isAlterAmbiguousAccidental(pitch.Accidental) && ! pitch.AccidentalXml) {
+                    return;
+                }
                 MusicSheetCalculator.symbolFactory.addGraphicalAccidental(graphicalNote, pitch);
             } else {
                 if (isInCurrentAlterationsToKeyList) {
@@ -108,6 +114,10 @@ export class AccidentalCalculator {
         }
     }
 
+    private isAlterAmbiguousAccidental(accidental: AccidentalEnum): boolean {
+        return accidental === AccidentalEnum.SLASHFLAT || accidental === AccidentalEnum.QUARTERTONEFLAT;
+    }
+
     private reactOnKeyInstructionChange(): void {
         const noteEnums: NoteEnum[] = this.activeKeyInstruction.AlteratedNotes;
         let keyAccidentalType: AccidentalEnum;

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

@@ -293,9 +293,13 @@ export class EngravingRules {
     /** Position of fingering label in relation to corresponding note (left, right supported, above, below experimental) */
     public FingeringPosition: PlacementEnum;
     public FingeringPositionFromXML: boolean;
+    public FingeringPositionGrace: PlacementEnum;
     public FingeringInsideStafflines: boolean;
     public FingeringLabelFontHeight: number;
     public FingeringOffsetX: number;
+    public FingeringOffsetY: number;
+    public FingeringPaddingY: number;
+    public FingeringTextSize: number;
     /** Whether to render string numbers in classical scores, i.e. not the string numbers in tabs, but e.g. for violin. */
     public RenderStringNumbersClassical: boolean;
     /** This is not for tabs, but for classical scores, especially violin. */
@@ -614,11 +618,15 @@ export class EngravingRules {
         this.RenderKeySignatures = true;
         this.RenderTimeSignatures = true;
         this.ArticulationPlacementFromXML = true;
-        this.FingeringPosition = PlacementEnum.Left; // easier to get bounding box, and safer for vertical layout
+        this.FingeringPosition = PlacementEnum.AboveOrBelow; // AboveOrBelow = correct bounding boxes
         this.FingeringPositionFromXML = true;
+        this.FingeringPositionGrace = PlacementEnum.Left;
         this.FingeringInsideStafflines = false;
         this.FingeringLabelFontHeight = 1.7;
         this.FingeringOffsetX = 0.0;
+        this.FingeringOffsetY = 0.0;
+        this.FingeringPaddingY = -0.2;
+        this.FingeringTextSize = 1.5;
         this.RenderStringNumbersClassical = true;
         this.StringNumberOffsetY = 0.0;
         this.NewSystemAtXMLNewSystemAttribute = false;

+ 23 - 0
src/MusicalScore/Graphical/GraphicalMeasure.ts

@@ -313,6 +313,29 @@ export abstract class GraphicalMeasure extends GraphicalObject {
         }
     }
 
+    public isPianoRightHand(): boolean {
+        return this.isUpperStaffOfInstrument();
+    }
+
+    public isPianoLeftHand(): boolean {
+        return this.isLowerStaffOfInstrument();
+    }
+
+    public isUpperStaffOfInstrument(): boolean {
+        if (this.parentStaff.ParentInstrument.Staves.length === 1) {
+            return true;
+        }
+        return this.ParentStaff === this.parentStaff.ParentInstrument.Staves[0];
+    }
+
+    public isLowerStaffOfInstrument(): boolean {
+        if (this.parentStaff.ParentInstrument.Staves.length === 1) {
+            return false; // technically this could be true as well, but we want this to be treated as upper and not return the same value.
+            // e.g. for a violin, fingerings should go above.
+        }
+        return this.ParentStaff === this.ParentStaff.ParentInstrument.Staves.last();
+    }
+
     public beginsWithLineRepetition(): boolean {
         const sourceMeasure: SourceMeasure = this.parentSourceMeasure;
         if (!sourceMeasure) {

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

@@ -18,6 +18,7 @@ import {CollectionUtil} from "../../Util/CollectionUtil";
 import { GraphicalVoiceEntry } from "./GraphicalVoiceEntry";
 import { MusicSheetCalculator } from "./MusicSheetCalculator";
 import { Tie } from "../VoiceData/Tie";
+import { GraphicalLabel } from "./GraphicalLabel";
 
 /**
  * The graphical counterpart of a [[SourceStaffEntry]].
@@ -38,6 +39,7 @@ export abstract class GraphicalStaffEntry extends GraphicalObject {
         if (sourceStaffEntry) {
             this.relInMeasureTimestamp = sourceStaffEntry.Timestamp;
         }
+        this.FingeringEntries = [];
     }
 
     public graphicalChordContainers: GraphicalChordSymbolContainer[] = [];
@@ -57,6 +59,7 @@ export abstract class GraphicalStaffEntry extends GraphicalObject {
     public ties: Tie[] = [];
     private graphicalTies: GraphicalTie[] = [];
     private lyricsEntries: GraphicalLyricEntry[] = [];
+    public FingeringEntries: GraphicalLabel[];
 
     public get GraphicalInstructions(): AbstractGraphicalInstruction[] {
         return this.graphicalInstructions;

+ 76 - 0
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -858,6 +858,7 @@ export abstract class MusicSheetCalculator {
                 this.calculateMeasureNumberPlacement(musicSystem);
             }
         }
+        this.calculateFingerings(); // if this is done after slurs, fingerings can be on top of slurs
         // calculate Slurs
         if (!this.leadSheet && this.rules.RenderSlurs) {
             this.calculateSlurs();
@@ -2551,6 +2552,81 @@ export abstract class MusicSheetCalculator {
         }
     }
 
+    public calculateFingerings(): void {
+        if (this.rules.FingeringPosition === PlacementEnum.Left ||
+            this.rules.FingeringPosition === PlacementEnum.Right) {
+                return;
+        }
+        for (const system of this.musicSystems) {
+            for (const line of system.StaffLines) {
+                for (const measure of line.Measures) {
+                    const placement: PlacementEnum = measure.isUpperStaffOfInstrument() ? PlacementEnum.Above : PlacementEnum.Below;
+                    for (const gse of measure.staffEntries) {
+                        gse.FingeringEntries = [];
+                        const skybottomcalculator: SkyBottomLineCalculator = line.SkyBottomLineCalculator;
+                        const staffEntryPositionX: number = gse.PositionAndShape.RelativePosition.x +
+                            measure.PositionAndShape.RelativePosition.x;
+                        const fingerings: TechnicalInstruction[] = [];
+                        for (const voiceEntry of gse.graphicalVoiceEntries) {
+                            for (const note of voiceEntry.notes) {
+                                const sourceNote: Note = note.sourceNote;
+                                if (sourceNote.Fingering && !sourceNote.IsGraceNote) {
+                                    fingerings.push(sourceNote.Fingering);
+                                }
+                            }
+                        }
+                        if (placement === PlacementEnum.Below) {
+                            fingerings.reverse();
+                        }
+                        for (let i: number = 0; i < fingerings.length; i++) {
+                            const fingering: TechnicalInstruction = fingerings[i];
+                            const alignment: TextAlignmentEnum =
+                                placement === PlacementEnum.Above ? TextAlignmentEnum.CenterBottom : TextAlignmentEnum.CenterTop;
+                            const label: Label = new Label(fingering.value, alignment);
+                            const gLabel: GraphicalLabel = new GraphicalLabel(
+                                label, this.rules.FingeringTextSize, label.textAlignment, this.rules, line.PositionAndShape);
+                            const marginLeft: number = staffEntryPositionX + gLabel.PositionAndShape.BorderMarginLeft;
+                            const marginRight: number = staffEntryPositionX + gLabel.PositionAndShape.BorderMarginRight;
+                            let skybottomFurthest: number = undefined;
+                            if (placement === PlacementEnum.Above) {
+                                skybottomFurthest = skybottomcalculator.getSkyLineMinInRange(marginLeft, marginRight);
+                            } else {
+                                skybottomFurthest = skybottomcalculator.getBottomLineMaxInRange(marginLeft, marginRight);
+                            }
+                            let yShift: number = 0;
+                            if (i === 0) {
+                                yShift += this.rules.FingeringOffsetY;
+                                if (placement === PlacementEnum.Above) {
+                                    yShift += 0.1; // above fingerings are a bit closer to the notes than below ones for some reason
+                                }
+                            } else {
+                                yShift += this.rules.FingeringPaddingY;
+                            }
+                            if (placement === PlacementEnum.Above) {
+                                yShift *= -1;
+                            }
+                            gLabel.PositionAndShape.RelativePosition.y += skybottomFurthest + yShift;
+                            gLabel.PositionAndShape.RelativePosition.x = staffEntryPositionX;
+                            gLabel.setLabelPositionAndShapeBorders();
+                            gLabel.PositionAndShape.calculateBoundingBox();
+                            gse.FingeringEntries.push(gLabel);
+                            const start: number = gLabel.PositionAndShape.RelativePosition.x + gLabel.PositionAndShape.BorderLeft;
+                            //start -= line.PositionAndShape.RelativePosition.x;
+                            const end: number = start - gLabel.PositionAndShape.BorderLeft + gLabel.PositionAndShape.BorderRight;
+                            if (placement === PlacementEnum.Above) {
+                                skybottomcalculator.updateSkyLineInRange(
+                                    start, end, gLabel.PositionAndShape.RelativePosition.y + gLabel.PositionAndShape.BorderTop); // BorderMarginTop too much
+                            } else if (placement === PlacementEnum.Below) {
+                                skybottomcalculator.updateBottomLineInRange(
+                                    start, end, gLabel.PositionAndShape.RelativePosition.y + gLabel.PositionAndShape.BorderBottom);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     private optimizeRestPlacement(): void {
         for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
             const system: MusicSystem = this.musicSystems[idx2];

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

@@ -1265,7 +1265,10 @@ export class VexFlowMeasure extends GraphicalMeasure {
 
                 // add fingering
                 if (voiceEntry.parentVoiceEntry && this.rules.RenderFingerings) {
-                    this.createFingerings(voiceEntry);
+                    if (this.rules.FingeringPosition === PlacementEnum.Left ||
+                        this.rules.FingeringPosition === PlacementEnum.Right) {
+                            this.createFingerings(voiceEntry);
+                    } // else created in MusicSheetCalculater.createFingerings() as Labels
                     this.createStringNumber(voiceEntry);
                 }
 
@@ -1392,6 +1395,15 @@ export class VexFlowMeasure extends GraphicalMeasure {
             }
             fingeringIndex++; // 0 for first fingering
             let fingeringPosition: PlacementEnum = this.rules.FingeringPosition;
+            //currently only relevant for grace notes, because we create other fingerings above/below in MusicSheetCalculator.createFingerings
+            if (this.rules.FingeringPositionGrace === PlacementEnum.AboveOrBelow) {
+                //if (this.rules.FingeringPosition === PlacementEnum.AboveOrBelow) {
+                if (this.isUpperStaffOfInstrument()) { // (e.g. piano right hand)
+                    fingeringPosition = PlacementEnum.Above;
+                } else if (this.isLowerStaffOfInstrument()) {
+                    fingeringPosition = PlacementEnum.Below;
+                }
+            }
             if (fingering.placement !== PlacementEnum.NotYetDefined) {
                 fingeringPosition = fingering.placement;
             }

+ 5 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts

@@ -321,6 +321,11 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
 
 
     private drawStaffEntry(staffEntry: GraphicalStaffEntry): void {
+        if (staffEntry.FingeringEntries.length > 0) {
+            for (const fingeringEntry of staffEntry.FingeringEntries) {
+                this.drawLabel(fingeringEntry, GraphicalLayers.Notes);
+            }
+        }
         // Draw ChordSymbols
         if (staffEntry.graphicalChordContainers !== undefined && staffEntry.graphicalChordContainers.length > 0) {
             for (const graphicalChordContainer of staffEntry.graphicalChordContainers) {

+ 3 - 2
src/MusicalScore/ScoreIO/VoiceGenerator.ts

@@ -367,6 +367,7 @@ export class VoiceGenerator {
                         stemColorXml: string, noteheadColorXml: string): Note {
     //log.debug("addSingleNote called");
     let noteAlter: number = 0;
+    let accidentalValue: string;
     let noteAccidental: AccidentalEnum = AccidentalEnum.NONE;
     let noteStep: NoteEnum = NoteEnum.C;
     let displayStepUnpitched: NoteEnum = NoteEnum.C;
@@ -423,7 +424,7 @@ export class VoiceGenerator {
 
           }
         } else if (noteElement.name === "accidental") {
-          const accidentalValue: string = noteElement.value;
+          accidentalValue = noteElement.value;
           if (accidentalValue === "natural") {
             noteAccidental = AccidentalEnum.NATURAL;
           } else if (accidentalValue === "slash-flat") {
@@ -468,7 +469,7 @@ export class VoiceGenerator {
     }
 
     noteOctave -= Pitch.OctaveXmlDifference;
-    const pitch: Pitch = new Pitch(noteStep, noteOctave, noteAccidental);
+    const pitch: Pitch = new Pitch(noteStep, noteOctave, noteAccidental, accidentalValue);
     const noteLength: Fraction = Fraction.createFromFraction(noteDuration);
     let note: Note = undefined;
     let stringNumber: number = -1;

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

@@ -27,6 +27,8 @@ export class AbstractExpression {
                 return PlacementEnum.Above;
             case "below":
                 return PlacementEnum.Below;
+            case "aboveorbelow":
+                return PlacementEnum.AboveOrBelow;
             case "left":
                 return PlacementEnum.Left;
             case "right":
@@ -43,5 +45,6 @@ export enum PlacementEnum {
     Below = 1,
     Left = 2,
     Right = 3,
-    NotYetDefined = 4
+    NotYetDefined = 4,
+    AboveOrBelow = 5, // for piano scores, above for right hand, below for left hand
 }

+ 197 - 0
test/data/test_Fingerings_Simple_Chords_Treble_Bass.musicxml

@@ -0,0 +1,197 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>Fingerings_Simple_Chords_Treble_Bass</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2021-11-10</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>7</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1697.14</page-height>
+      <page-width>1200</page-width>
+      <page-margins type="even">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="600" default-y="1611.43" justify="center" valign="top" font-size="22">Fingerings_Simple_Chords_Treble_Bass</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="334.90">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>65.90</left-margin>
+            <right-margin>627.77</right-margin>
+            </system-margins>
+          <top-system-distance>172.37</top-system-distance>
+          </system-layout>
+        <staff-layout number="2">
+          <staff-distance>65.00</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <staves>2</staves>
+        <clef number="1">
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        <clef number="2">
+          <sign>F</sign>
+          <line>4</line>
+          </clef>
+        </attributes>
+      <note default-x="86.99" default-y="-5.00">
+        <pitch>
+          <step>E</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        <staff>1</staff>
+        <notations>
+          <technical>
+            <fingering>1</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="86.99" default-y="5.00">
+        <chord/>
+        <pitch>
+          <step>G</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        <staff>1</staff>
+        <notations>
+          <technical>
+            <fingering>3</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="86.99" default-y="20.00">
+        <chord/>
+        <pitch>
+          <step>C</step>
+          <octave>6</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        <staff>1</staff>
+        <notations>
+          <technical>
+            <fingering>5</fingering>
+            </technical>
+          </notations>
+        </note>
+      <backup>
+        <duration>4</duration>
+        </backup>
+      <note default-x="86.99" default-y="-165.00">
+        <pitch>
+          <step>C</step>
+          <octave>2</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>5</voice>
+        <type>whole</type>
+        <staff>2</staff>
+        <notations>
+          <technical>
+            <fingering placement="below">4</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="86.99" default-y="-155.00">
+        <chord/>
+        <pitch>
+          <step>E</step>
+          <octave>2</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>5</voice>
+        <type>whole</type>
+        <staff>2</staff>
+        <notations>
+          <technical>
+            <fingering placement="below">2</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="86.99" default-y="-145.00">
+        <chord/>
+        <pitch>
+          <step>G</step>
+          <octave>2</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>5</voice>
+        <type>whole</type>
+        <staff>2</staff>
+        <notations>
+          <technical>
+            <fingering placement="below">1</fingering>
+            </technical>
+          </notations>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>