浏览代码

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.
 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.
 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:
 stavevolta.js:
 Fix the length of voltas for first measures in a system
 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:
 tabnote.js:
 Add a context group for each tabnote, so that it can be found in the SVG DOM ("vf-tabnote")
 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
 Currently, we are using Vexflow 1.2.93, because of some formatter advantages
 compared to Vexflow 3.x versions.
 compared to Vexflow 3.x versions.
 Because of that, we need to patch in a few fixes that came after 1.2.93.
 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();
     const ctx = stave.checkContext();
     this.setRendered();
     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
     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 top_y = stave.getYForTopText(stave.options.num_lines) + this.y_shift;
     const vert_height = 1.5 * stave.options.spacing_between_lines_px;
     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();
     this.setRendered();
     const render_stem = this.beam == null && this.render_options.draw_stem;
     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.context.openGroup('tabnote', null, { pointerBBox: true });
     this.drawPositions();
     this.drawPositions();
     this.drawStemThrough();
     this.drawStemThrough();

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

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