Browse Source

merge osmd-public/develop: fix multiline wedges (crescendo), fix single staffline drum whole note position, fix lyrics on secondary whole rest, fix StaffEntry.hasOnlyRests

sschmidTU 1 year ago
parent
commit
97b8af1ecd

+ 5 - 4
demo/embedded_demo.html

@@ -83,7 +83,8 @@
                     <td>openUrl</td>
                     <td>openUrl</td>
                     <td>valid URL pointing to a valid musicxml file</td>
                     <td>valid URL pointing to a valid musicxml file</td>
                     <td>"auto"</td>
                     <td>"auto"</td>
-                    <td>If given, this attribute open the given musicxml file and display it.
+                    <td>If given, this attribute open the given musicxml file and display it.<br>
+                        Note that you need to add <i>&endUrl</i> after the URL.
                         <p>
                         <p>
                             <h5>Important:</h5>
                             <h5>Important:</h5>
                             For now, the resources must be hosted on the same server, otherwise it will be blocked by
                             For now, the resources must be hosted on the same server, otherwise it will be blocked by
@@ -92,11 +93,11 @@
                         <p>
                         <p>
                             <h5>Examples:</h5>
                             <h5>Examples:</h5>
                             <a
                             <a
-                                href="http://localhost:8000/?embedded&openUrl=http://localhost:8000/BrookeWestSample.musicxml">
-                                http://localhost:8000/?embedded&openUrl=http://localhost:8000/BrookeWestSample.musicxml</a>
+                                href="http://localhost:8000/?embedded&openUrl=http://localhost:8000/BrookeWestSample.musicxml&endUrl">
+                                http://localhost:8000/?embedded&openUrl=http://localhost:8000/BrookeWestSample.musicxml&endUrl</a>
                                 <br>
                                 <br>
                             <a
                             <a
-                                href="http://localhost:8000/?embedded&openUrl=https://wpmedia.musicxml.com/wp-content/uploads/2017/12/MahlFaGe4Sample.mxl">http://localhost:8000/?embedded&openUrl=https://wpmedia.musicxml.com/wp-content/uploads/2017/12/MahlFaGe4Sample.mxl</a>
+                                href="http://localhost:8000/?embedded&openUrl=https://wpmedia.musicxml.com/wp-content/uploads/2017/12/MahlFaGe4Sample.mxl&endUrl">http://localhost:8000/?embedded&openUrl=https://wpmedia.musicxml.com/wp-content/uploads/2017/12/MahlFaGe4Sample.mxl&endUrl</a>
 
 
                         </p>
                         </p>
                     </td>
                     </td>

+ 43 - 0
src/MusicalScore/Graphical/GraphicalContinuousDynamicExpression.ts

@@ -260,6 +260,49 @@ export class GraphicalContinuousDynamicExpression extends AbstractGraphicalExpre
         }
         }
     }
     }
 
 
+    /** Wrapper for createFirstHalfCrescendoLines and createFirstHalfDiminuendoLines, agnostic of crescendo/diminuendo. */
+    public createFirstHalfLines(startX: number, endX: number, y: number,
+        wedgeOpeningLength: number = this.rules.WedgeOpeningLength,
+        wedgeMeasureEndOpeningLength: number = this.rules.WedgeMeasureEndOpeningLength,
+        wedgeLineWidth: number = this.rules.WedgeLineWidth
+    ): void {
+        if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
+            this.createFirstHalfCrescendoLines(startX, endX, y,
+                wedgeMeasureEndOpeningLength, wedgeLineWidth);
+        } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
+            this.createFirstHalfDiminuendoLines(startX, endX, y,
+                wedgeOpeningLength, wedgeMeasureEndOpeningLength, wedgeLineWidth);
+        }
+    }
+
+    /** Wrapper for createSecondHalfCrescendoLines and createSecondHalfDiminuendoLines, agnostic of crescendo/diminuendo. */
+    public createSecondHalfLines(startX: number, endX: number, y: number,
+        wedgeMeasureBeginOpeningLength: number = this.rules.WedgeMeasureBeginOpeningLength,
+        wedgeOpeningLength: number = this.rules.WedgeOpeningLength,
+        wedgeLineWidth: number = this.rules.WedgeLineWidth
+    ): void {
+        if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
+            this.createSecondHalfCrescendoLines(startX, endX, y,
+                wedgeMeasureBeginOpeningLength, wedgeOpeningLength, wedgeLineWidth);
+        } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
+            this.createSecondHalfDiminuendoLines(startX, endX, y,
+                wedgeMeasureBeginOpeningLength, wedgeLineWidth);
+        }
+    }
+
+    /** Wrapper for createCrescendoLines and createDiminuendoLines, agnostic of crescendo/decrescendo. */
+    public createLines(startX: number, endX: number, y: number,
+        wedgeOpeningLength: number = this.rules.WedgeOpeningLength, wedgeLineWidth: number = this.rules.WedgeLineWidth
+    ): void {
+        if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
+            this.createCrescendoLines(startX, endX, y,
+                wedgeOpeningLength, wedgeLineWidth);
+        } else if (this.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
+            this.createDiminuendoLines(startX, endX, y,
+                wedgeOpeningLength, wedgeLineWidth);
+        }
+    }
+
     /**
     /**
      * Calculate the BoundingBox (as a box around the Wedge).
      * Calculate the BoundingBox (as a box around the Wedge).
      */
      */

+ 1 - 10
src/MusicalScore/Graphical/GraphicalStaffEntry.ts

@@ -305,16 +305,7 @@ export abstract class GraphicalStaffEntry extends GraphicalObject {
      * Returns true if this staff entry has only rests
      * Returns true if this staff entry has only rests
      */
      */
     public hasOnlyRests(): boolean {
     public hasOnlyRests(): boolean {
-        const hasOnlyRests: boolean = true;
-        for (const gve of this.graphicalVoiceEntries) {
-            for (const graphicalNote of gve.notes) {
-                const note: Note = graphicalNote.sourceNote;
-                if (!note.isRest()) {
-                    return false;
-                }
-            }
-        }
-        return hasOnlyRests;
+        return this.sourceStaffEntry.hasOnlyRests;
     }
     }
 
 
     public getSkylineMin(): number {
     public getSkylineMin(): number {

+ 104 - 48
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -661,10 +661,11 @@ export abstract class MusicSheetCalculator {
                     this.rules.LyricsYOffsetToStaffHeight;
                     this.rules.LyricsYOffsetToStaffHeight;
 
 
                 // Y-position calculated according to aforementioned mapping
                 // Y-position calculated according to aforementioned mapping
-                let position: number = firstPosition + (this.rules.VerticalBetweenLyricsDistance + this.rules.LyricsHeight) * sortedLyricVerseNumberIndex;
-                if (this.leadSheet) {
-                    position = 3.4 + (this.rules.VerticalBetweenLyricsDistance + this.rules.LyricsHeight) * (sortedLyricVerseNumberIndex);
-                }
+                const position: number = firstPosition + (this.rules.VerticalBetweenLyricsDistance + this.rules.LyricsHeight) * sortedLyricVerseNumberIndex;
+                // TODO not sure what this leadsheet lyrics positioning was supposed to be, but it seems to ALWAYS put the lyrics inside the stafflines now.
+                // if (this.leadSheet) {
+                //     position = 3.4 + (this.rules.VerticalBetweenLyricsDistance + this.rules.LyricsHeight) * (sortedLyricVerseNumberIndex);
+                // }
                 const previousRelativeX: number = lyricsEntryLabel.PositionAndShape.RelativePosition.x;
                 const previousRelativeX: number = lyricsEntryLabel.PositionAndShape.RelativePosition.x;
                 lyricsEntryLabel.PositionAndShape.RelativePosition = new PointF2D(previousRelativeX, position);
                 lyricsEntryLabel.PositionAndShape.RelativePosition = new PointF2D(previousRelativeX, position);
                 lyricsEntryLabel.Label.fontStyle = lyricEntry.LyricsEntry.FontStyle;
                 lyricsEntryLabel.Label.fontStyle = lyricEntry.LyricsEntry.FontStyle;
@@ -1431,28 +1432,63 @@ export abstract class MusicSheetCalculator {
         const placement: PlacementEnum = graphicalContinuousDynamic.ContinuousDynamic.Placement;
         const placement: PlacementEnum = graphicalContinuousDynamic.ContinuousDynamic.Placement;
 
 
         // if ContinuousDynamicExpression is given from wedge
         // if ContinuousDynamicExpression is given from wedge
-        let secondGraphicalContinuousDynamic: GraphicalContinuousDynamicExpression = undefined;
+        let endGraphicalContinuousDynamic: GraphicalContinuousDynamicExpression = undefined;
 
 
         // last length check
         // last length check
         if (sameStaffLine && endPosInStaffLine.x - startPosInStaffline.x < this.rules.WedgeMinLength && !isSoftAccent) {
         if (sameStaffLine && endPosInStaffLine.x - startPosInStaffline.x < this.rules.WedgeMinLength && !isSoftAccent) {
             endPosInStaffLine.x = startPosInStaffline.x + this.rules.WedgeMinLength;
             endPosInStaffLine.x = startPosInStaffline.x + this.rules.WedgeMinLength;
         }
         }
 
 
-        // Upper staff wedge always starts at the given position and the lower staff wedge always starts at the begin of measure
+        // First staff wedge always starts at the given position and the last and inbetween wedges always start at the begin of measure
+        //   TODO: rename upper / lower to first / last, now that we can have inbetween wedges, though this creates a huge diff, and this should be clear now.
         const upperStartX: number = startPosInStaffline.x;
         const upperStartX: number = startPosInStaffline.x;
         let lowerStartX: number = endStaffLine.Measures[0].beginInstructionsWidth - this.rules.WedgeHorizontalMargin - 2;
         let lowerStartX: number = endStaffLine.Measures[0].beginInstructionsWidth - this.rules.WedgeHorizontalMargin - 2;
         //TODO fix this when a range of measures to draw is given that doesn't include all the dynamic's measures (e.g. for crescendo)
         //TODO fix this when a range of measures to draw is given that doesn't include all the dynamic's measures (e.g. for crescendo)
         let upperEndX: number = 0;
         let upperEndX: number = 0;
         let lowerEndX: number = 0;
         let lowerEndX: number = 0;
 
 
+        /** Wedges between first and last staffline, in case we span more than 2 stafflines. */
+        const inbetweenWedges: GraphicalContinuousDynamicExpression[] = [];
         if (!sameStaffLine) {
         if (!sameStaffLine) {
+            // add wedge in all stafflines between (including) start and end measure
             upperEndX = staffLine.PositionAndShape.Size.width;
             upperEndX = staffLine.PositionAndShape.Size.width;
             lowerEndX = endPosInStaffLine.x;
             lowerEndX = endPosInStaffLine.x;
 
 
-            // must create a new Wedge
-            secondGraphicalContinuousDynamic = new GraphicalContinuousDynamicExpression(
+            // get all stafflines between start measure and end measure, and add wedges for them.
+            //   This would be less lines of code if there was already a list of stafflines for the sheet.
+            const stafflinesCovered: StaffLine[] = [staffLine, endStaffLine]; // start and end staffline already get a wedge
+            const startMeasure: GraphicalMeasure = graphicalContinuousDynamic.StartMeasure;
+            let nextMeasure: GraphicalMeasure = startMeasure;
+            let iterations: number = 0; // safety measure against infinite loop
+            let sourceMeasureIndex: number = startMeasure.parentSourceMeasure.measureListIndex;
+            while (nextMeasure !== endMeasure && iterations < 1000) {
+                const nextSourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[sourceMeasureIndex];
+                const potentialNextMeasure: GraphicalMeasure = this.graphicalMusicSheet.getGraphicalMeasureFromSourceMeasureAndIndex(
+                    nextSourceMeasure, staffIndex
+                );
+                if (potentialNextMeasure) {
+                    nextMeasure = potentialNextMeasure;
+                    const nextStaffline: StaffLine = nextMeasure.ParentStaffLine;
+                    if (!stafflinesCovered.includes(nextStaffline)) {
+                        stafflinesCovered.push(nextStaffline);
+                        const newWedge: GraphicalContinuousDynamicExpression =
+                            new GraphicalContinuousDynamicExpression(
+                                graphicalContinuousDynamic.ContinuousDynamic,
+                                nextStaffline,
+                                nextStaffline.Measures[0].parentSourceMeasure
+                            );
+                        newWedge.IsSplittedPart = true;
+                        inbetweenWedges.push(newWedge);
+                    }
+                }
+                sourceMeasureIndex++;
+                iterations++;
+            }
+
+            // last wedge at endMeasure
+            endGraphicalContinuousDynamic = new GraphicalContinuousDynamicExpression(
                 graphicalContinuousDynamic.ContinuousDynamic, endStaffLine, endMeasure.parentSourceMeasure);
                 graphicalContinuousDynamic.ContinuousDynamic, endStaffLine, endMeasure.parentSourceMeasure);
-            secondGraphicalContinuousDynamic.IsSplittedPart = true;
+            endGraphicalContinuousDynamic.IsSplittedPart = true;
             graphicalContinuousDynamic.IsSplittedPart = true;
             graphicalContinuousDynamic.IsSplittedPart = true;
         } else {
         } else {
             upperEndX = endPosInStaffLine.x;
             upperEndX = endPosInStaffLine.x;
@@ -1471,7 +1507,7 @@ export abstract class MusicSheetCalculator {
 
 
         // the Height of the Expression's placement
         // the Height of the Expression's placement
         let idealY: number = 0;
         let idealY: number = 0;
-        let secondIdealY: number = 0;
+        let endIdealY: number = 0;
 
 
         if (placement === PlacementEnum.Below) {
         if (placement === PlacementEnum.Below) {
             // can be a single Staff Instrument or an Instrument with 2 Staves
             // can be a single Staff Instrument or an Instrument with 2 Staves
@@ -1501,7 +1537,7 @@ export abstract class MusicSheetCalculator {
             if (!sameStaffLine) {
             if (!sameStaffLine) {
                 // Set the value for the splitted y position to the ideal position before we check and modify it with
                 // Set the value for the splitted y position to the ideal position before we check and modify it with
                 // the skybottom calculator detection
                 // the skybottom calculator detection
-                secondIdealY = idealY;
+                endIdealY = idealY;
             }
             }
             // must check BottomLine for possible collisions within the Length of the Expression
             // must check BottomLine for possible collisions within the Length of the Expression
             // find the corresponding max value for the given Length
             // find the corresponding max value for the given Length
@@ -1552,12 +1588,12 @@ export abstract class MusicSheetCalculator {
             if (!sameStaffLine) {
             if (!sameStaffLine) {
                 maxBottomLineValueForExpressionLength = endStaffLine.SkyBottomLineCalculator.getBottomLineMaxInRange(lowerStartX, lowerEndX);
                 maxBottomLineValueForExpressionLength = endStaffLine.SkyBottomLineCalculator.getBottomLineMaxInRange(lowerStartX, lowerEndX);
 
 
-                if (maxBottomLineValueForExpressionLength > secondIdealY) {
-                    secondIdealY = maxBottomLineValueForExpressionLength;
+                if (maxBottomLineValueForExpressionLength > endIdealY) {
+                    endIdealY = maxBottomLineValueForExpressionLength;
                 }
                 }
 
 
-                secondIdealY += this.rules.WedgeOpeningLength / 2;
-                secondIdealY += this.rules.WedgeVerticalMargin;
+                endIdealY += this.rules.WedgeOpeningLength / 2;
+                endIdealY += this.rules.WedgeVerticalMargin;
             }
             }
 
 
             if (!withinCrossedBeam) {
             if (!withinCrossedBeam) {
@@ -1589,7 +1625,7 @@ export abstract class MusicSheetCalculator {
             // must consider the upperWedge starting/ending tip for the comparison with the SkyLine
             // must consider the upperWedge starting/ending tip for the comparison with the SkyLine
             idealY += this.rules.WedgeOpeningLength / 2;
             idealY += this.rules.WedgeOpeningLength / 2;
             if (!sameStaffLine) {
             if (!sameStaffLine) {
-                secondIdealY = idealY;
+                endIdealY = idealY;
             }
             }
 
 
             // must check SkyLine for possible collisions within the Length of the Expression
             // must check SkyLine for possible collisions within the Length of the Expression
@@ -1634,11 +1670,11 @@ export abstract class MusicSheetCalculator {
             if (!sameStaffLine) {
             if (!sameStaffLine) {
                 minSkyLineValueForExpressionLength = endStaffLine.SkyBottomLineCalculator.getSkyLineMinInRange(lowerStartX, lowerEndX);
                 minSkyLineValueForExpressionLength = endStaffLine.SkyBottomLineCalculator.getSkyLineMinInRange(lowerStartX, lowerEndX);
 
 
-                if (minSkyLineValueForExpressionLength < secondIdealY) {
-                    secondIdealY = minSkyLineValueForExpressionLength;
+                if (minSkyLineValueForExpressionLength < endIdealY) {
+                    endIdealY = minSkyLineValueForExpressionLength;
                 }
                 }
 
 
-                secondIdealY -= this.rules.WedgeOpeningLength / 2;
+                endIdealY -= this.rules.WedgeOpeningLength / 2;
             }
             }
 
 
             if (!withinCrossedBeam) {
             if (!withinCrossedBeam) {
@@ -1646,45 +1682,65 @@ export abstract class MusicSheetCalculator {
                 idealY -= this.rules.WedgeVerticalMargin;
                 idealY -= this.rules.WedgeVerticalMargin;
             }
             }
             if (!sameStaffLine) {
             if (!sameStaffLine) {
-                secondIdealY -= this.rules.WedgeVerticalMargin;
+                endIdealY -= this.rules.WedgeVerticalMargin;
             }
             }
         }
         }
 
 
         // now we have the correct placement Height for the Expression
         // now we have the correct placement Height for the Expression
         // the idealY is calculated relative to the currentStaffLine
         // the idealY is calculated relative to the currentStaffLine
 
 
-        // Crescendo (point to the left, opening to the right)
         graphicalContinuousDynamic.Lines.clear();
         graphicalContinuousDynamic.Lines.clear();
-        if (graphicalContinuousDynamic.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
-            if (isSoftAccent) {
-                graphicalContinuousDynamic.createFirstHalfCrescendoLines(upperStartX, upperEndX, idealY);
-                graphicalContinuousDynamic.createSecondHalfDiminuendoLines(lowerStartX, lowerEndX, idealY);
-                graphicalContinuousDynamic.calcPsi();
-                // secondGraphicalContinuousDynamic.createSecondHalfDiminuendoLines(lowerStartX, lowerEndX, idealY);
-                // secondGraphicalContinuousDynamic.calcPsi();
-            } else if (sameStaffLine && !isSoftAccent) {
-                graphicalContinuousDynamic.createCrescendoLines(upperStartX, upperEndX, idealY);
-                graphicalContinuousDynamic.calcPsi();
-            } else {
-                // two different Wedges
-                graphicalContinuousDynamic.createFirstHalfCrescendoLines(upperStartX, upperEndX, idealY);
-                graphicalContinuousDynamic.calcPsi();
+        if (isSoftAccent) {
+            // either createFirstHalfCrescendoLines or createFirstHalfDiminuendoLines, same principle / parameters.
+            graphicalContinuousDynamic.createFirstHalfCrescendoLines(upperStartX, upperEndX, idealY);
+            graphicalContinuousDynamic.createSecondHalfDiminuendoLines(lowerStartX, lowerEndX, idealY);
+            graphicalContinuousDynamic.calcPsi();
+        } else if (sameStaffLine && !isSoftAccent) {
+            graphicalContinuousDynamic.createLines(upperStartX, upperEndX, idealY);
+            graphicalContinuousDynamic.calcPsi();
+        } else {
+            // two+ different Wedges
+            // first wedge
+            graphicalContinuousDynamic.createFirstHalfLines(upperStartX, upperEndX, idealY);
+            graphicalContinuousDynamic.calcPsi();
+
+            // inbetween wedges
+            for (let i: number = 0; i < inbetweenWedges.length; i++) {
+                const inbetweenWedge: GraphicalContinuousDynamicExpression = inbetweenWedges[i];
+                const inbetweenStaffline: StaffLine = inbetweenWedge.ParentStaffLine;
+                let betweenIdealY: number = endIdealY;
+
+                if (placement === PlacementEnum.Below) {
+                    const maxBottomLineValueForExpressionLength: number =
+                    endStaffLine.SkyBottomLineCalculator.getBottomLineMaxInRange(lowerStartX, upperEndX);
+                    if (maxBottomLineValueForExpressionLength > betweenIdealY) {
+                        betweenIdealY = maxBottomLineValueForExpressionLength;
+                    }
+                    betweenIdealY += this.rules.WedgeOpeningLength / 2;
+                    betweenIdealY += this.rules.WedgeVerticalMargin;
+                } else if (placement === PlacementEnum.Above) {
+                    const minSkyLineValueForExpressionLength: number =
+                        inbetweenStaffline.SkyBottomLineCalculator.getSkyLineMinInRange(lowerStartX, lowerEndX);
+                    if (minSkyLineValueForExpressionLength < endIdealY) {
+                        betweenIdealY = minSkyLineValueForExpressionLength;
+                    }
+                    betweenIdealY -= this.rules.WedgeOpeningLength / 2;
+                }
 
 
-                secondGraphicalContinuousDynamic.createSecondHalfCrescendoLines(lowerStartX, lowerEndX, secondIdealY);
-                secondGraphicalContinuousDynamic.calcPsi();
+                if (graphicalContinuousDynamic.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
+                    inbetweenWedge.createSecondHalfCrescendoLines(0, inbetweenStaffline.PositionAndShape.Size.width, betweenIdealY);
+                    // for crescendo, we want the same look as on the last staffline: not starting with an intersection / starting wedge
+                } else {
+                    inbetweenWedge.createFirstHalfDiminuendoLines(0, inbetweenStaffline.PositionAndShape.Size.width, betweenIdealY);
+                    // for diminuendo, we want the same look as on the first staffline: not ending in an intersection / looking finished
+                }
+                inbetweenWedge.calcPsi();
             }
             }
-        } else if (graphicalContinuousDynamic.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo) {
-            if (sameStaffLine) {
-                graphicalContinuousDynamic.createDiminuendoLines(upperStartX, upperEndX, idealY);
-                graphicalContinuousDynamic.calcPsi();
-            } else {
-                graphicalContinuousDynamic.createFirstHalfDiminuendoLines(upperStartX, upperEndX, idealY);
-                graphicalContinuousDynamic.calcPsi();
 
 
-                secondGraphicalContinuousDynamic.createSecondHalfDiminuendoLines(lowerStartX, lowerEndX, secondIdealY);
-                secondGraphicalContinuousDynamic.calcPsi();
-            }
-        } //End Diminuendo
+            // last wedge
+            endGraphicalContinuousDynamic.createSecondHalfLines(lowerStartX, lowerEndX, endIdealY);
+            endGraphicalContinuousDynamic.calcPsi();
+        }
         this.dynamicExpressionMap.set(endAbsoluteTimestamp.RealValue, graphicalContinuousDynamic.PositionAndShape);
         this.dynamicExpressionMap.set(endAbsoluteTimestamp.RealValue, graphicalContinuousDynamic.PositionAndShape);
     }
     }
 
 

+ 3 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts

@@ -327,6 +327,9 @@ export class VexFlowConverter {
                     baseNoteLength.RealValue === note.sourceNote.SourceMeasure.ActiveTimeSignature.RealValue;
                     baseNoteLength.RealValue === note.sourceNote.SourceMeasure.ActiveTimeSignature.RealValue;
                 if (isWholeMeasureRest) {
                 if (isWholeMeasureRest) {
                     keys = ["d/5"];
                     keys = ["d/5"];
+                    if (gve.parentStaffEntry.parentMeasure.ParentStaff.StafflineCount === 1) {
+                        keys = ["b/4"];
+                    }
                     duration = "w";
                     duration = "w";
                     numDots = 0;
                     numDots = 0;
                     // If it's a whole rest we want it smack in the middle. Apparently there is still an issue in vexflow:
                     // If it's a whole rest we want it smack in the middle. Apparently there is still an issue in vexflow:

+ 3 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -640,6 +640,9 @@ export class VexFlowMeasure extends GraphicalMeasure {
                     for (let i: number = 0; i < this.tuplets[voiceID].length; i++) {
                     for (let i: number = 0; i < this.tuplets[voiceID].length; i++) {
                         const tuplet: Tuplet = this.tuplets[voiceID][i][0];
                         const tuplet: Tuplet = this.tuplets[voiceID][i][0];
                         const vftuplet: VF.Tuplet = this.vftuplets[voiceID][i];
                         const vftuplet: VF.Tuplet = this.vftuplets[voiceID][i];
+                        if (!vftuplet) { // see #1330, potentially to be investigated. why undefined?
+                            continue;
+                        }
                         if (!tuplet.RenderTupletNumber) {
                         if (!tuplet.RenderTupletNumber) {
                             // (vftuplet as any).numerator_glyphs_stored = [...(vftuplet as any).numerator_glyphs];
                             // (vftuplet as any).numerator_glyphs_stored = [...(vftuplet as any).numerator_glyphs];
                             // (vftuplet as any).numerator_glyphs = [];
                             // (vftuplet as any).numerator_glyphs = [];

+ 21 - 3
src/MusicalScore/Graphical/VexFlow/VexFlowStaffEntry.ts

@@ -8,6 +8,7 @@ import { unitInPixels } from "./VexFlowMusicSheetDrawer";
 import { VexFlowVoiceEntry } from "./VexFlowVoiceEntry";
 import { VexFlowVoiceEntry } from "./VexFlowVoiceEntry";
 import { Note } from "../../VoiceData/Note";
 import { Note } from "../../VoiceData/Note";
 import { AccidentalEnum } from "../../../Common/DataObjects/Pitch";
 import { AccidentalEnum } from "../../../Common/DataObjects/Pitch";
+import { BoundingBox } from "../BoundingBox";
 
 
 export class VexFlowStaffEntry extends GraphicalStaffEntry {
 export class VexFlowStaffEntry extends GraphicalStaffEntry {
     constructor(measure: VexFlowMeasure, sourceStaffEntry: SourceStaffEntry, staffEntryParent: VexFlowStaffEntry) {
     constructor(measure: VexFlowMeasure, sourceStaffEntry: SourceStaffEntry, staffEntryParent: VexFlowStaffEntry) {
@@ -36,18 +37,35 @@ export class VexFlowStaffEntry extends GraphicalStaffEntry {
                     continue;
                     continue;
                 }
                 }
                 gve.applyBordersFromVexflow();
                 gve.applyBordersFromVexflow();
+                let isSecondaryWholeRest: boolean = false;
+                let bboxToAdjust: BoundingBox = this.PositionAndShape;
+                if (gve.notes[0].sourceNote.isWholeRest() && !this.hasOnlyRests()) {
+                    isSecondaryWholeRest = true;
+                    // continue; // also an option (simpler), but makes the voice entry bounding boxes very wrong (shifted)
+                    bboxToAdjust = gve.PositionAndShape;
+                    // don't use a whole rest's position for the staffentry.x if we also have a normal note in another voice (#1267)
+                    //   a more ideal solution would probably be to give a secondary whole note its own staffentry and staffentry position,
+                    //   since it's so different from a normal note which is also the first note of the measure.
+                    //   But we probably have some code that assumes there's only one staffentry per staff per timestamp.
+                    //   "A [[SourceStaffEntry]] is a container spanning all the [[VoiceEntry]]s at one timestamp for one [[StaffLine]]"
+                }
                 if (this.parentMeasure.ParentStaff.isTab) {
                 if (this.parentMeasure.ParentStaff.isTab) {
                     // the x-position could be finetuned for the cursor.
                     // the x-position could be finetuned for the cursor.
                     // somehow, gve.vfStaveNote.getBoundingBox() is null for a TabNote (which is a StemmableNote).
                     // somehow, gve.vfStaveNote.getBoundingBox() is null for a TabNote (which is a StemmableNote).
-                    this.PositionAndShape.RelativePosition.x = (gve.vfStaveNote.getAbsoluteX() + (<any>gve.vfStaveNote).glyph.getWidth()) / unitInPixels;
+                    bboxToAdjust.RelativePosition.x = (gve.vfStaveNote.getAbsoluteX() + (<any>gve.vfStaveNote).glyph.getWidth()) / unitInPixels;
                 } else {
                 } else {
-                    this.PositionAndShape.RelativePosition.x = gve.vfStaveNote.getBoundingBox().getX() / unitInPixels;
+                    bboxToAdjust.RelativePosition.x = gve.vfStaveNote.getBoundingBox().getX() / unitInPixels;
+                    if (isSecondaryWholeRest) {
+                        bboxToAdjust.RelativePosition.x -= stave.getNoteStartX() / unitInPixels;
+                        bboxToAdjust.RelativePosition.x -= 1.3;
+                        // fix whole rest bounding box for these cases, slightly hacky admittedly, probably depends on WholeRestXShiftVexflow
+                    }
                 }
                 }
                 const sourceNote: Note = gve.notes[0].sourceNote;
                 const sourceNote: Note = gve.notes[0].sourceNote;
                 if (sourceNote.isRest() && sourceNote.Length.RealValue === this.parentMeasure.parentSourceMeasure.ActiveTimeSignature.RealValue) {
                 if (sourceNote.isRest() && sourceNote.Length.RealValue === this.parentMeasure.parentSourceMeasure.ActiveTimeSignature.RealValue) {
                     // whole rest: length = measure length. (4/4 in a 4/4 time signature, 3/4 in a 3/4 time signature, 1/4 in a 1/4 time signature, etc.)
                     // whole rest: length = measure length. (4/4 in a 4/4 time signature, 3/4 in a 3/4 time signature, 1/4 in a 1/4 time signature, etc.)
                     // see Note.isWholeRest(), which is currently not safe
                     // see Note.isWholeRest(), which is currently not safe
-                    this.PositionAndShape.RelativePosition.x +=
+                    bboxToAdjust.RelativePosition.x +=
                         this.parentMeasure.parentSourceMeasure.Rules.WholeRestXShiftVexflow - 0.1; // xShift from VexFlowConverter
                         this.parentMeasure.parentSourceMeasure.Rules.WholeRestXShiftVexflow - 0.1; // xShift from VexFlowConverter
                     gve.PositionAndShape.BorderLeft = -0.7;
                     gve.PositionAndShape.BorderLeft = -0.7;
                     gve.PositionAndShape.BorderRight = 0.7;
                     gve.PositionAndShape.BorderRight = 0.7;

+ 1 - 1
src/MusicalScore/VoiceData/SourceStaffEntry.ts

@@ -270,7 +270,7 @@ export class SourceStaffEntry {
     public get hasOnlyRests(): boolean {
     public get hasOnlyRests(): boolean {
         for (const voiceEntry of this.voiceEntries) {
         for (const voiceEntry of this.voiceEntries) {
             for (const note of voiceEntry.Notes) {
             for (const note of voiceEntry.Notes) {
-                if (!note.isRest) {
+                if (!note.isRest()) {
                     return false;
                     return false;
                 }
                 }
             }
             }

+ 3 - 0
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -269,6 +269,9 @@ export class OpenSheetMusicDisplay {
                 cursor.init(this.sheet.MusicPartManager, this.graphic);
                 cursor.init(this.sheet.MusicPartManager, this.graphic);
             });
             });
         }
         }
+        if (this.drawingParameters.DrawingParametersEnum === DrawingParametersEnum.leadsheet) {
+            this.graphic.LeadSheet = true;
+        }
         this.renderingManager.setMusicSheet(this.graphic);
         this.renderingManager.setMusicSheet(this.graphic);
         this.interactionManager.Initialize();
         this.interactionManager.Initialize();
     }
     }

+ 4 - 1
src/VexFlowPatch/src/tables.js

@@ -83,7 +83,10 @@ Flow.keyProperties = (key, clef, params) => {
   let octave = parseInt(pieces[1], 10);
   let octave = parseInt(pieces[1], 10);
 
 
   // Octave_shift is the shift to compensate for clef 8va/8vb.
   // Octave_shift is the shift to compensate for clef 8va/8vb.
-  octave += -1 * options.octave_shift;
+  octave -= options.octave_shift;
+  // VexFlowPatch: change "+= -1 *" to "-=". No change in OSMD detected,
+  //   though apparently -0 can cause issues on some systems.
+  //   https://github.com/0xfe/vexflow/pull/1596
 
 
   const base_index = (octave * 7) - (4 * 7);
   const base_index = (octave * 7) - (4 * 7);
   let line = (base_index + value.index) / 2;
   let line = (base_index + value.index) / 2;

+ 6 - 1
test/Util/generateImages_browserless.mjs

@@ -327,6 +327,8 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
         isTestOctaveShiftInvisibleInstrument = sampleFilename.includes("test_octaveshift_first_instrument_invisible");
         isTestOctaveShiftInvisibleInstrument = sampleFilename.includes("test_octaveshift_first_instrument_invisible");
         const isTextOctaveShiftExtraGraphicalMeasure = sampleFilename.includes("test_octaveshift_extragraphicalmeasure");
         const isTextOctaveShiftExtraGraphicalMeasure = sampleFilename.includes("test_octaveshift_extragraphicalmeasure");
         isTestInvisibleMeasureNotAffectingLayout = sampleFilename.includes("test_invisible_measure_not_affecting_layout");
         isTestInvisibleMeasureNotAffectingLayout = sampleFilename.includes("test_invisible_measure_not_affecting_layout");
+        const isTestWedgeMultilineCrescendo = sampleFilename.includes("test_wedge_multiline_crescendo");
+        const isTestWedgeMultilineDecrescendo = sampleFilename.includes("test_wedge_multiline_decrescendo");
         osmdInstance.EngravingRules.loadDefaultValues(); // note this may also be executed in setOptions below via drawingParameters default
         osmdInstance.EngravingRules.loadDefaultValues(); // note this may also be executed in setOptions below via drawingParameters default
         if (isTestEndClefStaffEntryBboxes) {
         if (isTestEndClefStaffEntryBboxes) {
             drawBoundingBoxString = "VexFlowStaffEntry";
             drawBoundingBoxString = "VexFlowStaffEntry";
@@ -375,7 +377,10 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
         if (isTestCajon2NoteSystem) {
         if (isTestCajon2NoteSystem) {
             osmdInstance.EngravingRules.PercussionUseCajon2NoteSystem = true;
             osmdInstance.EngravingRules.PercussionUseCajon2NoteSystem = true;
         }
         }
-        if (isTextOctaveShiftExtraGraphicalMeasure || isTestOctaveShiftInvisibleInstrument) {
+        if (isTextOctaveShiftExtraGraphicalMeasure ||
+            isTestOctaveShiftInvisibleInstrument ||
+            isTestWedgeMultilineCrescendo ||
+            isTestWedgeMultilineDecrescendo) {
             osmdInstance.EngravingRules.NewSystemAtXMLNewSystemAttribute = true;
             osmdInstance.EngravingRules.NewSystemAtXMLNewSystemAttribute = true;
         }
         }
     }
     }

+ 309 - 0
test/data/test_lyrics_x-alignment_whole_rest_second_voice.musicxml

@@ -0,0 +1,309 @@
+<?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>test_lyrics_x-alignment_whole_rest_second_voice</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2022-11-07</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>rights</credit-type>
+    <credit-words default-x="600" default-y="85.7143" justify="center" valign="bottom" font-size="9">e</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name print-object="no">Piano</part-name>
+      <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-group type="start" number="1">
+      <group-symbol>brace</group-symbol>
+      </part-group>
+    <score-part id="P2">
+      <part-name print-object="no">Piano</part-name>
+      <score-instrument id="P2-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P2-I1" port="1"></midi-device>
+      <midi-instrument id="P2-I1">
+        <midi-channel>2</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="462.69">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>70.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <note default-x="83.49" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <lyric number="1" default-x="-20.39" default-y="-40.16" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>1. lorem</text>
+          </lyric>
+        <lyric number="2" default-x="-22.00" default-y="-61.82" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>2. lorem</text>
+          </lyric>
+        </note>
+      <note default-x="177.84" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <lyric number="1" default-x="6.50" default-y="-40.16" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>ipsum</text>
+          </lyric>
+        <lyric number="2" default-x="6.50" default-y="-61.82" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>ipsum</text>
+          </lyric>
+        </note>
+      <note default-x="272.19" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <lyric number="1" default-x="6.50" default-y="-40.16" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>dolor</text>
+          </lyric>
+        <lyric number="2" default-x="6.50" default-y="-61.82" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>dolor</text>
+          </lyric>
+        </note>
+      <note default-x="366.54" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <lyric number="1" default-x="6.50" default-y="-40.16" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>sit</text>
+          </lyric>
+        <lyric number="2" default-x="6.50" default-y="-61.82" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>sit</text>
+          </lyric>
+        </note>
+      <backup>
+        <duration>4</duration>
+        </backup>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>2</voice>
+        <type>whole</type>
+        </note>
+      </measure>
+    <measure number="2" width="515.88">
+      <note default-x="30.73" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <lyric number="1" default-x="6.50" default-y="-40.16" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>amet</text>
+          </lyric>
+        <lyric number="2" default-x="6.50" default-y="-61.82" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>amet</text>
+          </lyric>
+        </note>
+      <note default-x="149.36" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <lyric number="1" default-x="6.50" default-y="-40.16" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>consectetur</text>
+          </lyric>
+        <lyric number="2" default-x="6.50" default-y="-61.82" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>consectetur</text>
+          </lyric>
+        </note>
+      <note default-x="267.98" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <lyric number="1" default-x="6.50" default-y="-40.16" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>adipiscing</text>
+          </lyric>
+        <lyric number="2" default-x="6.50" default-y="-61.82" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>adipiscing</text>
+          </lyric>
+        </note>
+      <note default-x="386.61" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <lyric number="1" default-x="6.50" default-y="-40.16" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>elit</text>
+          </lyric>
+        <lyric number="2" default-x="6.50" default-y="-61.82" relative-y="-30.00">
+          <syllabic>single</syllabic>
+          <text>elit</text>
+          </lyric>
+        </note>
+      <backup>
+        <duration>4</duration>
+        </backup>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>2</voice>
+        <type>whole</type>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  <part id="P2">
+    <measure number="1" width="462.69">
+      <print>
+        <staff-layout number="1">
+          <staff-distance>147.81</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>F</sign>
+          <line>4</line>
+          </clef>
+        </attributes>
+      <note>
+        <rest measure="yes"/>
+        <duration>4</duration>
+        <voice>1</voice>
+        </note>
+      </measure>
+    <measure number="2" width="515.88">
+      <note>
+        <rest measure="yes"/>
+        <duration>4</duration>
+        <voice>1</voice>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 171 - 0
test/data/test_wedge_multiline_crescendo.musicxml

@@ -0,0 +1,171 @@
+<?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>test_wedge_multiline_crescendo</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2023-09-18</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>6.99911</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1596.77</page-height>
+      <page-width>1233.87</page-width>
+      <page-margins type="even">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</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="616.935" default-y="1511.05" justify="center" valign="top" font-size="22">test_wedge_multiline_crescendo</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="978.70">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="crescendo" number="1" default-y="-68.65"/>
+          </direction-type>
+        </direction>
+      <note default-x="84.22" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      </measure>
+    <measure number="2" width="1028.70">
+      <print new-system="yes">
+        <system-layout>
+          <system-margins>
+            <left-margin>0.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <system-distance>235.00</system-distance>
+          </system-layout>
+        </print>
+      <note default-x="58.59" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      </measure>
+    <measure number="3" width="1028.70">
+      <print new-system="yes">
+        <system-layout>
+          <system-margins>
+            <left-margin>0.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <system-distance>235.00</system-distance>
+          </system-layout>
+        </print>
+      <note default-x="58.59" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      </measure>
+    <measure number="4" width="212.21">
+      <print new-system="yes">
+        <system-layout>
+          <system-margins>
+            <left-margin>0.00</left-margin>
+            <right-margin>816.49</right-margin>
+            </system-margins>
+          <system-distance>235.00</system-distance>
+          </system-layout>
+        </print>
+      <note default-x="58.59" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 171 - 0
test/data/test_wedge_multiline_decrescendo.musicxml

@@ -0,0 +1,171 @@
+<?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>test_wedge_multiline_decrescendo</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2023-09-18</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>6.99911</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1596.77</page-height>
+      <page-width>1233.87</page-width>
+      <page-margins type="even">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</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="616.935" default-y="1511.05" justify="center" valign="top" font-size="22">test_wedge_multiline_decrescendo</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="978.70">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="diminuendo" number="1" default-y="-68.65"/>
+          </direction-type>
+        </direction>
+      <note default-x="84.22" default-y="-50.00">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      </measure>
+    <measure number="2" width="1028.70">
+      <print new-system="yes">
+        <system-layout>
+          <system-margins>
+            <left-margin>0.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <system-distance>235.00</system-distance>
+          </system-layout>
+        </print>
+      <note default-x="58.59" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      </measure>
+    <measure number="3" width="1028.70">
+      <print new-system="yes">
+        <system-layout>
+          <system-margins>
+            <left-margin>0.00</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <system-distance>235.00</system-distance>
+          </system-layout>
+        </print>
+      <note default-x="58.59" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      </measure>
+    <measure number="4" width="212.21">
+      <print new-system="yes">
+        <system-layout>
+          <system-margins>
+            <left-margin>0.00</left-margin>
+            <right-margin>816.49</right-margin>
+            </system-margins>
+          <system-distance>235.00</system-distance>
+          </system-layout>
+        </print>
+      <note default-x="58.59" default-y="-35.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>