Переглянути джерело

fix(EmptyMeasures): prevent a Vexflow bug where a measure was empty because a modifier width was NaN

fix #899
osmd-p #49

also, added more information for VexFlowPatch files
sschmid 4 роки тому
батько
коміт
a0dbc4f625

+ 15 - 0
src/VexFlowPatch/readme.txt

@@ -1,5 +1,17 @@
+VexFlowPatch
+base vexflow version:
+1.2.93
+
+note: this patch will likely create errors when used with a different vexflow version, like 3.x
+if using a different vexflow version, disable this prebuild patch script in package.json.
+
 These files are custom patches for the currently installed vexflow version.
 They are copied by the npm prebuild script to ../../node_modules/vexflow/src/ before a build.
+Each .js has comments like "//VexFlowPatch: [explanation]" to indicate what was changed.
+(a diff can be created from the base vexflow version)
+
+stave.js:
+prevent a bug where a modifier width is NaN, leading to a VexFlow error
 
 stavevolta.js:
 Fix the length of voltas for first measures in a system
@@ -8,6 +20,9 @@ Fix the length of voltas for first measures in a system
 tabnote.js:
 Add a context group for each tabnote, so that it can be found in the SVG DOM ("vf-tabnote")
 
+tremolo.js:
+Add extra_stroke_scale, y_spacing_scale
+
 Currently, we are using Vexflow 1.2.93, because of some formatter advantages
 compared to Vexflow 3.x versions.
 Because of that, we need to patch in a few fixes that came after 1.2.93.

+ 715 - 0
src/VexFlowPatch/src/stave.js

@@ -0,0 +1,715 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+
+import { Vex } from './vex';
+import { Element } from './element';
+import { Flow } from './tables';
+import { Barline } from './stavebarline';
+import { StaveModifier } from './stavemodifier';
+import { Repetition } from './staverepetition';
+import { StaveSection } from './stavesection';
+import { StaveTempo } from './stavetempo';
+import { StaveText } from './stavetext';
+import { BoundingBox } from './boundingbox';
+import { Clef } from './clef';
+import { KeySignature } from './keysignature';
+import { TimeSignature } from './timesignature';
+import { Volta } from './stavevolta';
+
+export class Stave extends Element {
+  constructor(x, y, width, options) {
+    super();
+    this.setAttribute('type', 'Stave');
+
+    this.x = x;
+    this.y = y;
+    this.width = width;
+    this.formatted = false;
+    this.setStartX(x + 5);
+    // this.start_x = x + 5;
+    this.end_x = x + width;
+    this.modifiers = [];  // stave modifiers (clef, key, time, barlines, coda, segno, etc.)
+    this.measure = 0;
+    this.clef = 'treble';
+    this.endClef = undefined;
+    this.font = {
+      family: 'sans-serif',
+      size: 8,
+      weight: '',
+    };
+    this.options = {
+      vertical_bar_width: 10,       // Width around vertical bar end-marker
+      glyph_spacing_px: 10,
+      num_lines: 5,
+      fill_style: '#999999',
+      left_bar: true,               // draw vertical bar on left
+      right_bar: true,               // draw vertical bar on right
+      spacing_between_lines_px: 10, // in pixels
+      space_above_staff_ln: 4,      // in staff lines
+      space_below_staff_ln: 4,      // in staff lines
+      top_text_position: 1,          // in staff lines
+    };
+    this.bounds = { x: this.x, y: this.y, w: this.width, h: 0 };
+    Vex.Merge(this.options, options);
+
+    this.resetLines();
+
+    const BARTYPE = Barline.type;
+    // beg bar
+    this.addModifier(new Barline(this.options.left_bar ? BARTYPE.SINGLE : BARTYPE.NONE));
+    // end bar
+    this.addEndModifier(new Barline(this.options.right_bar ? BARTYPE.SINGLE : BARTYPE.NONE));
+  }
+
+  space(spacing) { return this.options.spacing_between_lines_px * spacing; }
+
+  resetLines() {
+    this.options.line_config = [];
+    for (let i = 0; i < this.options.num_lines; i++) {
+      this.options.line_config.push({ visible: true });
+    }
+    this.height = (this.options.num_lines + this.options.space_above_staff_ln) *
+      this.options.spacing_between_lines_px;
+    this.options.bottom_text_position = this.options.num_lines;
+  }
+
+  getOptions() { return this.options; }
+
+  setNoteStartX(x) {
+    if (!this.formatted) this.format();
+
+    if (!(x >= 0)) {
+      console.log("NaN here1");
+    }
+    this.setStartX(x);
+    const begBarline = this.modifiers[0];
+    begBarline.setX(this.start_x - begBarline.getWidth());
+    return this;
+  }
+  setStartX(x) {
+    if (!(x >= 0)) {
+      console.log("vex: x not >= 0");
+    }
+    this.start_x = x;
+  }
+  getNoteStartX() {
+    if (!this.formatted) this.format();
+
+    return this.start_x;
+  }
+
+  getNoteEndX() {
+    if (!this.formatted) this.format();
+
+    return this.end_x;
+  }
+  getTieStartX() { return this.start_x; }
+  getTieEndX() { return this.x + this.width; }
+  getX() { return this.x; }
+  getNumLines() { return this.options.num_lines; }
+  setNumLines(lines) {
+    this.options.num_lines = parseInt(lines, 10);
+    this.resetLines();
+    return this;
+  }
+  setY(y) { this.y = y; return this; }
+
+  getTopLineTopY() {
+    return this.getYForLine(0) - (Flow.STAVE_LINE_THICKNESS / 2);
+  }
+  getBottomLineBottomY() {
+    return this.getYForLine(this.getNumLines() - 1) + (Flow.STAVE_LINE_THICKNESS / 2);
+  }
+
+  setX(x) {
+    const shift = x - this.x;
+    this.formatted = false;
+    this.x = x;
+    this.start_x += shift;
+    this.end_x += shift;
+    for (let i = 0; i < this.modifiers.length; i++) {
+      const mod = this.modifiers[i];
+      if (mod.x !== undefined) {
+        mod.x += shift;
+      }
+    }
+    return this;
+  }
+
+  setWidth(width) {
+    this.formatted = false;
+    this.width = width;
+    this.end_x = this.x + width;
+
+    // reset the x position of the end barline (TODO(0xfe): This makes no sense)
+    // this.modifiers[1].setX(this.end_x);
+    return this;
+  }
+
+  getWidth() {
+    return this.width;
+  }
+
+  getStyle() {
+    return {
+      fillStyle: this.options.fill_style,
+      strokeStyle: this.options.fill_style, // yes, this is correct for legacy compatibility
+      lineWidth: Flow.STAVE_LINE_THICKNESS, ...this.style || {}
+    };
+  }
+
+  setMeasure(measure) { this.measure = measure; return this; }
+
+  /**
+   * Gets the pixels to shift from the beginning of the stave
+   * following the modifier at the provided index
+   * @param  {Number} index The index from which to determine the shift
+   * @return {Number}       The amount of pixels shifted
+   */
+  getModifierXShift(index = 0) {
+    if (typeof index !== 'number') {
+      throw new Vex.RERR('InvalidIndex', 'Must be of number type');
+    }
+
+    if (!this.formatted) this.format();
+
+    if (this.getModifiers(StaveModifier.Position.BEGIN).length === 1) {
+      return 0;
+    }
+
+    let start_x = this.start_x - this.x;
+    const begBarline = this.modifiers[0];
+    if (begBarline.getType() === Barline.type.REPEAT_BEGIN && start_x > begBarline.getWidth()) {
+      start_x -= begBarline.getWidth();
+    }
+
+    return start_x;
+  }
+
+  // Coda & Segno Symbol functions
+  setRepetitionTypeLeft(type, y) {
+    this.modifiers.push(new Repetition(type, this.x, y));
+    return this;
+  }
+
+  setRepetitionTypeRight(type, y) {
+    this.modifiers.push(new Repetition(type, this.x, y));
+    return this;
+  }
+
+  // Volta functions
+  setVoltaType(type, number_t, y) {
+    this.modifiers.push(new Volta(type, number_t, this.x, y));
+    return this;
+  }
+
+  // Section functions
+  setSection(section, y) {
+    this.modifiers.push(new StaveSection(section, this.x, y));
+    return this;
+  }
+
+  // Tempo functions
+  setTempo(tempo, y) {
+    this.modifiers.push(new StaveTempo(tempo, this.x, y));
+    return this;
+  }
+
+  // Text functions
+  setText(text, position, options) {
+    this.modifiers.push(new StaveText(text, position, options));
+    return this;
+  }
+
+  getHeight() {
+    return this.height;
+  }
+
+  getSpacingBetweenLines() {
+    return this.options.spacing_between_lines_px;
+  }
+
+  getBoundingBox() {
+    return new BoundingBox(this.x, this.y, this.width, this.getBottomY() - this.y);
+  }
+
+  getBottomY() {
+    const options = this.options;
+    const spacing = options.spacing_between_lines_px;
+    const score_bottom = this.getYForLine(options.num_lines) +
+      (options.space_below_staff_ln * spacing);
+
+    return score_bottom;
+  }
+
+  getBottomLineY() {
+    return this.getYForLine(this.options.num_lines);
+  }
+
+  // This returns the y for the *center* of a staff line
+  getYForLine(line) {
+    const options = this.options;
+    const spacing = options.spacing_between_lines_px;
+    const headroom = options.space_above_staff_ln;
+
+    const y = this.y + (line * spacing) + (headroom * spacing);
+
+    return y;
+  }
+
+  getLineForY(y) {
+    // Does the reverse of getYForLine - somewhat dumb and just calls
+    // getYForLine until the right value is reaches
+
+    const options = this.options;
+    const spacing = options.spacing_between_lines_px;
+    const headroom = options.space_above_staff_ln;
+    return ((y - this.y) / spacing) - headroom;
+  }
+
+  getYForTopText(line) {
+    const l = line || 0;
+    return this.getYForLine(-l - this.options.top_text_position);
+  }
+
+  getYForBottomText(line) {
+    const l = line || 0;
+    return this.getYForLine(this.options.bottom_text_position + l);
+  }
+
+  getYForNote(line) {
+    const options = this.options;
+    const spacing = options.spacing_between_lines_px;
+    const headroom = options.space_above_staff_ln;
+    const y = this.y + (headroom * spacing) + (5 * spacing) - (line * spacing);
+
+    return y;
+  }
+
+  getYForGlyphs() {
+    return this.getYForLine(3);
+  }
+
+  // This method adds a stave modifier to the stave. Note that the first two
+  // modifiers (BarLines) are automatically added upon construction.
+  addModifier(modifier, position) {
+    if (position !== undefined) {
+      modifier.setPosition(position);
+    }
+
+    modifier.setStave(this);
+    this.formatted = false;
+    this.modifiers.push(modifier);
+    return this;
+  }
+
+  addEndModifier(modifier) {
+    this.addModifier(modifier, StaveModifier.Position.END);
+    return this;
+  }
+
+  // Bar Line functions
+  setBegBarType(type) {
+    // Only valid bar types at beginning of stave is none, single or begin repeat
+    const { SINGLE, REPEAT_BEGIN, NONE } = Barline.type;
+    if (type === SINGLE || type === REPEAT_BEGIN || type === NONE) {
+      this.modifiers[0].setType(type);
+      this.formatted = false;
+    }
+    return this;
+  }
+
+  setEndBarType(type) {
+    // Repeat end not valid at end of stave
+    if (type !== Barline.type.REPEAT_BEGIN) {
+      this.modifiers[1].setType(type);
+      this.formatted = false;
+    }
+    return this;
+  }
+
+  setClef(clefSpec, size, annotation, position) {
+    if (position === undefined) {
+      position = StaveModifier.Position.BEGIN;
+    }
+
+    if (position === StaveModifier.Position.END) {
+      this.endClef = clefSpec;
+    } else {
+      this.clef = clefSpec;
+    }
+
+    const clefs = this.getModifiers(position, Clef.CATEGORY);
+    if (clefs.length === 0) {
+      this.addClef(clefSpec, size, annotation, position);
+    } else {
+      clefs[0].setType(clefSpec, size, annotation);
+    }
+
+    return this;
+  }
+
+  setEndClef(clefSpec, size, annotation) {
+    this.setClef(clefSpec, size, annotation, StaveModifier.Position.END);
+    return this;
+  }
+
+  setKeySignature(keySpec, cancelKeySpec, position) {
+    if (position === undefined) {
+      position = StaveModifier.Position.BEGIN;
+    }
+
+    const keySignatures = this.getModifiers(position, KeySignature.CATEGORY);
+    if (keySignatures.length === 0) {
+      this.addKeySignature(keySpec, cancelKeySpec, position);
+    } else {
+      keySignatures[0].setKeySig(keySpec, cancelKeySpec);
+    }
+
+    return this;
+  }
+
+  setEndKeySignature(keySpec, cancelKeySpec) {
+    this.setKeySignature(keySpec, cancelKeySpec, StaveModifier.Position.END);
+    return this;
+  }
+
+  setTimeSignature(timeSpec, customPadding, position) {
+    if (position === undefined) {
+      position = StaveModifier.Position.BEGIN;
+    }
+
+    const timeSignatures = this.getModifiers(position, TimeSignature.CATEGORY);
+    if (timeSignatures.length === 0) {
+      this.addTimeSignature(timeSpec, customPadding, position);
+    } else {
+      timeSignatures[0].setTimeSig(timeSpec);
+    }
+
+    return this;
+  }
+
+  setEndTimeSignature(timeSpec, customPadding) {
+    this.setTimeSignature(timeSpec, customPadding, StaveModifier.Position.END);
+    return this;
+  }
+
+  addKeySignature(keySpec, cancelKeySpec, position) {
+    if (position === undefined) {
+      position = StaveModifier.Position.BEGIN;
+    }
+    this.addModifier(new KeySignature(keySpec, cancelKeySpec)
+      .setPosition(position), position);
+    return this;
+  }
+
+  addClef(clef, size, annotation, position) {
+    if (position === undefined || position === StaveModifier.Position.BEGIN) {
+      this.clef = clef;
+    } else if (position === StaveModifier.Position.END) {
+      this.endClef = clef;
+    }
+
+    this.addModifier(new Clef(clef, size, annotation), position);
+    return this;
+  }
+
+  addEndClef(clef, size, annotation) {
+    this.addClef(clef, size, annotation, StaveModifier.Position.END);
+    return this;
+  }
+
+  addTimeSignature(timeSpec, customPadding, position) {
+    this.addModifier(new TimeSignature(timeSpec, customPadding), position);
+    return this;
+  }
+
+  addEndTimeSignature(timeSpec, customPadding) {
+    this.addTimeSignature(timeSpec, customPadding, StaveModifier.Position.END);
+    return this;
+  }
+
+  // Deprecated
+  addTrebleGlyph() {
+    this.addClef('treble');
+    return this;
+  }
+
+  getModifiers(position, category) {
+    if (position === undefined && category === undefined) return this.modifiers;
+
+    return this.modifiers.filter(modifier =>
+      (position === undefined || position === modifier.getPosition()) &&
+      (category === undefined || category === modifier.getCategory())
+    );
+  }
+
+  sortByCategory(items, order) {
+    for (let i = items.length - 1; i >= 0; i--) {
+      for (let j = 0; j < i; j++) {
+        if (order[items[j].getCategory()] > order[items[j + 1].getCategory()]) {
+          const temp = items[j];
+          items[j] = items[j + 1];
+          items[j + 1] = temp;
+        }
+      }
+    }
+  }
+
+  format() {
+    const begBarline = this.modifiers[0];
+    const endBarline = this.modifiers[1];
+
+    const begModifiers = this.getModifiers(StaveModifier.Position.BEGIN);
+    const endModifiers = this.getModifiers(StaveModifier.Position.END);
+
+    this.sortByCategory(begModifiers, {
+      barlines: 0, clefs: 1, keysignatures: 2, timesignatures: 3,
+    });
+
+    this.sortByCategory(endModifiers, {
+      timesignatures: 0, keysignatures: 1, barlines: 2, clefs: 3,
+    });
+
+    if (begModifiers.length > 1 &&
+      begBarline.getType() === Barline.type.REPEAT_BEGIN) {
+      begModifiers.push(begModifiers.splice(0, 1)[0]);
+      begModifiers.splice(0, 0, new Barline(Barline.type.SINGLE));
+    }
+
+    if (endModifiers.indexOf(endBarline) > 0) {
+      endModifiers.splice(0, 0, new Barline(Barline.type.NONE));
+    }
+
+    let width;
+    let padding;
+    let modifier;
+    let offset = 0;
+    let x = this.x;
+    for (let i = 0; i < begModifiers.length; i++) {
+      modifier = begModifiers[i];
+      padding = modifier.getPadding(i + offset);
+      width = modifier.getWidth();
+      // VexFlowPatch: prevent modifier width being NaN and throwing Vexflow error
+      if (isNaN(width)) {
+        modifier.setWidth(10);
+        width = 10;
+      }
+
+      x += padding;
+      modifier.setX(x);
+      x += width;
+
+      if (padding + width === 0) offset--;
+    }
+
+    this.setStartX(x);
+    // this.start_x = x;
+    x = this.x + this.width;
+
+    const widths = {
+      left: 0,
+      right: 0,
+      paddingRight: 0,
+      paddingLeft: 0,
+    };
+
+    let lastBarlineIdx = 0;
+
+    for (let i = 0; i < endModifiers.length; i++) {
+      modifier = endModifiers[i];
+      lastBarlineIdx = (modifier.getCategory() === 'barlines') ? i : lastBarlineIdx;
+
+      widths.right = 0;
+      widths.left = 0;
+      widths.paddingRight = 0;
+      widths.paddingLeft = 0;
+      const layoutMetrics = modifier.getLayoutMetrics();
+
+      if (layoutMetrics) {
+        if (i !== 0) {
+          widths.right = layoutMetrics.xMax || 0;
+          widths.paddingRight = layoutMetrics.paddingRight || 0;
+        }
+        widths.left = (-layoutMetrics.xMin) || 0;
+        widths.paddingLeft = layoutMetrics.paddingLeft || 0;
+
+        if (i === endModifiers.length - 1) {
+          widths.paddingLeft = 0;
+        }
+      } else {
+        widths.paddingRight = modifier.getPadding(i - lastBarlineIdx);
+        if (i !== 0) {
+          widths.right = modifier.getWidth();
+        }
+        if (i === 0) {
+          widths.left = modifier.getWidth();
+        }
+      }
+      x -= widths.paddingRight;
+      x -= widths.right;
+
+      modifier.setX(x);
+
+      x -= widths.left;
+      x -= widths.paddingLeft;
+    }
+
+    this.end_x = endModifiers.length === 1 ? this.x + this.width : x;
+    this.formatted = true;
+  }
+
+  /**
+   * All drawing functions below need the context to be set.
+   */
+  draw() {
+    this.checkContext();
+    this.setRendered();
+
+    if (!this.formatted) this.format();
+
+    const num_lines = this.options.num_lines;
+    const width = this.width;
+    const x = this.x;
+    let y;
+
+    // Render lines
+    for (let line = 0; line < num_lines; line++) {
+      y = this.getYForLine(line);
+
+      this.applyStyle();
+      if (this.options.line_config[line].visible) {
+        this.context.beginPath();
+        this.context.moveTo(x, y);
+        this.context.lineTo(x + width, y);
+        this.context.stroke();
+      }
+      this.restoreStyle();
+    }
+
+    // Draw the modifiers (bar lines, coda, segno, repeat brackets, etc.)
+    for (let i = 0; i < this.modifiers.length; i++) {
+      // Only draw modifier if it has a draw function
+      if (typeof this.modifiers[i].draw === 'function') {
+        this.modifiers[i].applyStyle(this.context);
+        this.modifiers[i].draw(this, this.getModifierXShift(i));
+        this.modifiers[i].restoreStyle(this.context);
+      }
+    }
+
+    // Render measure numbers
+    if (this.measure > 0) {
+      this.context.save();
+      this.context.setFont(this.font.family, this.font.size, this.font.weight);
+      const text_width = this.context.measureText('' + this.measure).width;
+      y = this.getYForTopText(0) + 3;
+      this.context.fillText('' + this.measure, this.x - text_width / 2, y);
+      this.context.restore();
+    }
+
+    return this;
+  }
+
+  // Draw Simple barlines for backward compatability
+  // Do not delete - draws the beginning bar of the stave
+  drawVertical(x, isDouble) {
+    this.drawVerticalFixed(this.x + x, isDouble);
+  }
+
+  drawVerticalFixed(x, isDouble) {
+    this.checkContext();
+
+    const top_line = this.getYForLine(0);
+    const bottom_line = this.getYForLine(this.options.num_lines - 1);
+    if (isDouble) {
+      this.context.fillRect(x - 3, top_line, 1, bottom_line - top_line + 1);
+    }
+    this.context.fillRect(x, top_line, 1, bottom_line - top_line + 1);
+  }
+
+  drawVerticalBar(x) {
+    this.drawVerticalBarFixed(this.x + x, false);
+  }
+
+  drawVerticalBarFixed(x) {
+    this.checkContext();
+
+    const top_line = this.getYForLine(0);
+    const bottom_line = this.getYForLine(this.options.num_lines - 1);
+    this.context.fillRect(x, top_line, 1, bottom_line - top_line + 1);
+  }
+
+  /**
+   * Get the current configuration for the Stave.
+   * @return {Array} An array of configuration objects.
+   */
+  getConfigForLines() {
+    return this.options.line_config;
+  }
+
+  /**
+   * Configure properties of the lines in the Stave
+   * @param line_number The index of the line to configure.
+   * @param line_config An configuration object for the specified line.
+   * @throws Vex.RERR "StaveConfigError" When the specified line number is out of
+   *   range of the number of lines specified in the constructor.
+   */
+  setConfigForLine(line_number, line_config) {
+    if (line_number >= this.options.num_lines || line_number < 0) {
+      throw new Vex.RERR(
+        'StaveConfigError',
+        'The line number must be within the range of the number of lines in the Stave.'
+      );
+    }
+
+    if (line_config.visible === undefined) {
+      throw new Vex.RERR(
+        'StaveConfigError',
+        "The line configuration object is missing the 'visible' property."
+      );
+    }
+
+    if (typeof (line_config.visible) !== 'boolean') {
+      throw new Vex.RERR(
+        'StaveConfigError',
+        "The line configuration objects 'visible' property must be true or false."
+      );
+    }
+
+    this.options.line_config[line_number] = line_config;
+
+    return this;
+  }
+
+  /**
+   * Set the staff line configuration array for all of the lines at once.
+   * @param lines_configuration An array of line configuration objects.  These objects
+   *   are of the same format as the single one passed in to setLineConfiguration().
+   *   The caller can set null for any line config entry if it is desired that the default be used
+   * @throws Vex.RERR "StaveConfigError" When the lines_configuration array does not have
+   *   exactly the same number of elements as the num_lines configuration object set in
+   *   the constructor.
+   */
+  setConfigForLines(lines_configuration) {
+    if (lines_configuration.length !== this.options.num_lines) {
+      throw new Vex.RERR(
+        'StaveConfigError',
+        'The length of the lines configuration array must match the number of lines in the Stave'
+      );
+    }
+
+    // Make sure the defaults are present in case an incomplete set of
+    //  configuration options were supplied.
+    // eslint-disable-next-line
+    for (const line_config in lines_configuration) {
+      // Allow 'null' to be used if the caller just wants the default for a particular node.
+      if (!lines_configuration[line_config]) {
+        lines_configuration[line_config] = this.options.line_config[line_config];
+      }
+      Vex.Merge(this.options.line_config[line_config], lines_configuration[line_config]);
+    }
+
+    this.options.line_config = lines_configuration;
+
+    return this;
+  }
+}

+ 1 - 0
src/VexFlowPatch/src/stavevolta.js

@@ -36,6 +36,7 @@ export class Volta extends StaveModifier {
     const ctx = stave.checkContext();
     this.setRendered();
 
+    // VexFlowPatch: don't add x. already merged in Vexflow 3.x
     let width = stave.width - x; // don't add x offset to width
     const top_y = stave.getYForTopText(stave.options.num_lines) + this.y_shift;
     const vert_height = 1.5 * stave.options.spacing_between_lines_px;

+ 1 - 0
src/VexFlowPatch/src/tabnote.js

@@ -470,6 +470,7 @@ export class TabNote extends StemmableNote {
     this.setRendered();
     const render_stem = this.beam == null && this.render_options.draw_stem;
 
+    // VexFlowPatch: open group for tabnote, so that the SVG DOM has a named element for tabnote, like stavenote
     this.context.openGroup('tabnote', null, { pointerBBox: true });
     this.drawPositions();
     this.drawStemThrough();

+ 2 - 0
src/VexFlowPatch/src/tremolo.js

@@ -39,11 +39,13 @@ export class Tremolo extends Modifier {
 
     this.setRendered();
     const stemDirection = this.note.getStemDirection();
+    // VexFlowPatch:add y_spacing_scale
     this.y_spacing = 4 * stemDirection * this.y_spacing_scale;
     const start = this.note.getModifierStartXY(this.position, this.index);
     let x = start.x;
     let y = this.note.stem.getExtents().topY;
     let scale = this.note.getCategory() === 'gracenotes' ? GraceNote.SCALE : 1;
+    // VexFlowPatch: add extra stroke scale
     scale *= this.extra_stroke_scale;
     if (stemDirection < 0) {
       y += Tremolo.YOFFSETSTEMDOWN * scale;