Przeglądaj źródła

feat(alignRests): add auto option (alignRests: 2) which only aligns rests if measure contains simultaneous voices

recommended option, could in rare cases deteriorate previous layout

part of #621
sschmid 5 lat temu
rodzic
commit
1c8de9f677

+ 5 - 5
src/MusicalScore/Graphical/EngravingRules.ts

@@ -3,7 +3,7 @@ import { PagePlacementEnum } from "./GraphicalMusicPage";
 import * as log from "loglevel";
 import { TextAlignmentEnum } from "../../Common/Enums/TextAlignment";
 import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
-import { AutoBeamOptions } from "../../OpenSheetMusicDisplay/OSMDOptions";
+import { AutoBeamOptions, AlignRestOption } from "../../OpenSheetMusicDisplay/OSMDOptions";
 import { ColoringModes as ColoringMode } from "./DrawingParameters";
 import { Dictionary } from "typescript-collections";
 import { NoteEnum } from "../..";
@@ -180,7 +180,7 @@ export class EngravingRules {
     private durationDistanceDict: {[_: number]: number; } = {};
     private durationScalingDistanceDict: {[_: number]: number; } = {};
 
-    private alignRests: boolean;
+    private alignRests: number; // 0 = false, 1 = true, 2 = auto
     private drawSlurs: boolean;
     private coloringMode: ColoringMode;
     private coloringEnabled: boolean;
@@ -405,7 +405,7 @@ export class EngravingRules {
         this.metronomeMarkYShift = -0.5;
 
         // Render options (whether to render specific or invisible elements)
-        this.alignRests = false;
+        this.alignRests = AlignRestOption.False; // 0 = false, 1 = true, 2 = auto
         this.drawSlurs = true;
         this.coloringMode = ColoringMode.XML;
         this.coloringEnabled = true;
@@ -1360,10 +1360,10 @@ export class EngravingRules {
     public get DurationScalingDistanceDict(): {[_: number]: number; } {
         return this.durationScalingDistanceDict;
     }
-    public get AlignRests(): boolean {
+    public get AlignRests(): number {
         return this.alignRests;
     }
-    public set AlignRests(value: boolean) {
+    public set AlignRests(value: number) {
         this.alignRests = value;
     }
     public get DrawSlurs(): boolean {

+ 59 - 13
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -46,6 +46,7 @@ import { BoundingBox } from "../BoundingBox";
 import { ContinuousDynamicExpression } from "../../VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression";
 import { VexFlowContinuousDynamicExpression } from "./VexFlowContinuousDynamicExpression";
 import { InstantaneousTempoExpression } from "../../VoiceData/Expressions";
+import { AlignRestOption } from "../../../OpenSheetMusicDisplay";
 
 export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
   /** space needed for a dash for lyrics spacing, calculated once */
@@ -132,26 +133,71 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
       //     formatter.format(allVoices, w);
       // };
       MusicSheetCalculator.setMeasuresMinStaffEntriesWidth(measures, minStaffEntriesWidth);
+
+      const formatVoicesDefault: (w: number) => void = (w) => {
+        formatter.format(allVoices, w);
+      };
+      const formatVoicesAlignRests: (w: number) => void = (w) => {
+        formatter.format(allVoices, w, {
+          align_rests: true,
+          context: undefined
+        });
+      };
+
       for (const measure of measures) {
+        // determine whether to align rests
+        if (EngravingRules.Rules.AlignRests === AlignRestOption.False) {
+          (measure as VexFlowMeasure).formatVoices = formatVoicesDefault;
+        } else if (EngravingRules.Rules.AlignRests === AlignRestOption.True) {
+          (measure as VexFlowMeasure).formatVoices = formatVoicesAlignRests;
+        } else if (EngravingRules.Rules.AlignRests === AlignRestOption.Auto) {
+          let alignRests: boolean = false;
+          for (const staffEntry of measure.staffEntries) {
+            let collidableVoiceEntries: number = 0;
+            let numberOfRests: number = 0;
+            for (const voiceEntry of staffEntry.graphicalVoiceEntries) {
+              if (!voiceEntry.parentVoiceEntry.IsGrace) {
+                if (voiceEntry && voiceEntry.notes && voiceEntry.notes[0] && voiceEntry.notes[0].sourceNote) {// TODO null chaining, TS 3.7
+                  if (voiceEntry.notes[0].sourceNote.PrintObject) { // only respect collision when not invisible
+                    collidableVoiceEntries++;
+                  }
+                }
+              }
+              if (voiceEntry && voiceEntry.notes && voiceEntry.notes[0] && voiceEntry.notes[0].sourceNote) {// TODO null chaining, TS 3.7
+                if (voiceEntry.notes[0].sourceNote.isRest() && voiceEntry.notes[0].sourceNote.PrintObject) {
+                  numberOfRests++; // only align rests if there is actually a rest (which could collide)
+                }
+              }
+              if (collidableVoiceEntries > 1 && numberOfRests >= 1) {
+                // TODO could add further checks like if any of the already checked voice entries actually collide
+                alignRests = true;
+                break;
+              }
+            }
+            if (alignRests) {
+              break;
+            }
+          }
+
+          // set measure's format function
+          if (alignRests) {
+            (measure as VexFlowMeasure).formatVoices = formatVoicesAlignRests;
+          } else {
+            (measure as VexFlowMeasure).formatVoices = formatVoicesDefault;
+          }
+        }
+
+        // format first measure with minimum width
         if (measure === measures[0]) {
           const vexflowMeasure: VexFlowMeasure = (measure as VexFlowMeasure);
           // prepare format function for voices, will be called later for formatting measure again
-          let formatVoices: (w: number) => void = (w) => {
-            formatter.format(allVoices, w);
-          };
-          if (EngravingRules.Rules.AlignRests) { // aligns rests and avoids rest collisions. has to be enabled in OSMDOptions.alignRests
-            formatVoices = (w) => {
-              formatter.format(allVoices, w, {
-              align_rests: true,
-              context: undefined
-              });
-            };
-          }
-          vexflowMeasure.formatVoices = formatVoices;
+          //vexflowMeasure.formatVoices = formatVoicesDefault;
+
           // format now for minimum width, calculateMeasureWidthFromLyrics later
           vexflowMeasure.formatVoices(minStaffEntriesWidth * unitInPixels);
         } else {
-          (measure as VexFlowMeasure).formatVoices = undefined;
+          //(measure as VexFlowMeasure).formatVoices = undefined;
+          // TODO why was the formatVoices function disabled for other measures? would now disable the new align rests option.
         }
       }
     }

+ 11 - 3
src/OpenSheetMusicDisplay/OSMDOptions.ts

@@ -5,11 +5,13 @@ import { DrawingParametersEnum, ColoringModes } from "../MusicalScore/Graphical/
  *  Example: osmd.setOptions({defaultColorRest: "#AAAAAA", drawSubtitle: false}); osmd.render();
  */
 export interface IOSMDOptions {
-    /** Whether to let Vexflow align rests to preceding or following notes (Vexflow option). Default false.
+    /** Whether to let Vexflow align rests to preceding or following notes (Vexflow option). Default false (0).
      * This can naturally reduce collisions of rest notes with other notes.
-     * This also changes the position of rests when there is no simultaneous note at the same x-coordinate.
+     * Auto mode (2) only aligns rests when there are multiple voices in a measure, and at least once at the same x-coordinate.
+     * Auto is the recommended setting, and would be default, if it couldn't in rare cases deteriorate rest placement for existing users.
+     * The on mode (1) always aligns rests, also changing their position when there is no simultaneous note at the same x-coordinate, which is nonstandard.
      */
-    alignRests?: boolean;
+    alignRests?: AlignRestOption | number;
     /** Whether to automatically create beams for notes that don't have beams set in XML. */
     autoBeam?: boolean;
     /** Options for autoBeaming like whether to beam over rests. See AutoBeamOptions interface. */
@@ -108,6 +110,12 @@ export interface IOSMDOptions {
     drawHiddenNotes?: boolean;
 }
 
+export enum AlignRestOption {
+    False = 0,
+    True = 1,
+    Auto = 2
+}
+
 /** Handles [[IOSMDOptions]], e.g. returning default options with OSMDOptionsStandard() */
 export class OSMDOptions {
     /** Returns the default options for OSMD.