vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
922 lines (780 loc) • 27.5 kB
text/typescript
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// MIT License
import { BoundingBox, Bounds } from './boundingbox';
import { Clef } from './clef';
import { Element, ElementStyle } from './element';
import { Font, FontInfo, FontStyle, FontWeight } from './font';
import { KeySignature } from './keysignature';
import { Barline, BarlineType } from './stavebarline';
import { StaveModifier, StaveModifierPosition } from './stavemodifier';
import { Repetition } from './staverepetition';
import { StaveSection } from './stavesection';
import { StaveTempo, StaveTempoOptions } from './stavetempo';
import { StaveText } from './stavetext';
import { Volta } from './stavevolta';
import { Tables } from './tables';
import { TimeSignature } from './timesignature';
import { Category, isBarline } from './typeguard';
import { RuntimeError } from './util';
export interface StaveLineConfig {
visible?: boolean;
}
export interface StaveOptions {
bottom_text_position?: number;
line_config?: StaveLineConfig[];
space_below_staff_ln?: number;
space_above_staff_ln?: number;
vertical_bar_width?: number;
fill_style?: string;
left_bar?: boolean;
right_bar?: boolean;
spacing_between_lines_px?: number;
top_text_position?: number;
num_lines?: number;
}
// Used by Stave.format() to sort the modifiers at the beginning and end of a stave.
// The keys (computed property names) match the CATEGORY property in the
// Barline, Clef, KeySignature, TimeSignature classes.
const SORT_ORDER_BEG_MODIFIERS = {
[Barline.CATEGORY]: 0,
[Clef.CATEGORY]: 1,
[KeySignature.CATEGORY]: 2,
[TimeSignature.CATEGORY]: 3,
};
const SORT_ORDER_END_MODIFIERS = {
[TimeSignature.CATEGORY]: 0,
[KeySignature.CATEGORY]: 1,
[Barline.CATEGORY]: 2,
[Clef.CATEGORY]: 3,
};
export class Stave extends Element {
static get CATEGORY(): string {
return Category.Stave;
}
static TEXT_FONT: Required<FontInfo> = {
family: Font.SANS_SERIF,
size: 8,
weight: FontWeight.NORMAL,
style: FontStyle.NORMAL,
};
readonly options: Required<StaveOptions>;
protected start_x: number;
protected clef: string;
protected endClef?: string;
protected x: number;
protected y: number;
protected width: number;
// Initialized by the constructor via this.resetLines().
protected height: number = 0;
protected formatted: boolean;
protected end_x: number;
protected measure: number;
protected bounds: Bounds;
protected readonly modifiers: StaveModifier[];
protected defaultLedgerLineStyle: ElementStyle;
// This is the sum of the padding that normally goes on left + right of a stave during
// drawing. Used to size staves correctly with content width.
static get defaultPadding(): number {
const musicFont = Tables.currentMusicFont();
return musicFont.lookupMetric('stave.padding') + musicFont.lookupMetric('stave.endPaddingMax');
}
// Right padding, used by system if startX is already determined.
static get rightPadding(): number {
const musicFont = Tables.currentMusicFont();
return musicFont.lookupMetric('stave.endPaddingMax');
}
constructor(x: number, y: number, width: number, options?: StaveOptions) {
super();
this.x = x;
this.y = y;
this.width = width;
this.formatted = false;
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.resetFont();
this.options = {
vertical_bar_width: 10, // Width around vertical bar end-marker
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: Tables.STAVE_LINE_DISTANCE, // 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
bottom_text_position: 4, // in staff lines
line_config: [],
...options,
};
this.bounds = { x: this.x, y: this.y, w: this.width, h: 0 };
this.defaultLedgerLineStyle = { strokeStyle: '#444', lineWidth: 1.4 };
this.resetLines();
// beg bar
this.addModifier(new Barline(this.options.left_bar ? BarlineType.SINGLE : BarlineType.NONE));
// end bar
this.addEndModifier(new Barline(this.options.right_bar ? BarlineType.SINGLE : BarlineType.NONE));
}
/** Set default style for ledger lines. */
setDefaultLedgerLineStyle(style: ElementStyle): void {
this.defaultLedgerLineStyle = style;
}
/** Get default style for ledger lines. */
getDefaultLedgerLineStyle(): ElementStyle {
return { ...this.getStyle(), ...this.defaultLedgerLineStyle };
}
space(spacing: number): number {
return this.options.spacing_between_lines_px * spacing;
}
resetLines(): void {
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;
}
setNoteStartX(x: number): this {
if (!this.formatted) this.format();
this.start_x = x;
return this;
}
getNoteStartX(): number {
if (!this.formatted) this.format();
return this.start_x;
}
getNoteEndX(): number {
if (!this.formatted) this.format();
return this.end_x;
}
getTieStartX(): number {
return this.start_x;
}
getTieEndX(): number {
return this.end_x;
}
getX(): number {
return this.x;
}
getNumLines(): number {
return this.options.num_lines;
}
setNumLines(n: number): this {
this.options.num_lines = n;
this.resetLines();
return this;
}
setY(y: number): this {
this.y = y;
return this;
}
getY(): number {
return this.y;
}
getTopLineTopY(): number {
return this.getYForLine(0) - Tables.STAVE_LINE_THICKNESS / 2;
}
getBottomLineBottomY(): number {
return this.getYForLine(this.getNumLines() - 1) + Tables.STAVE_LINE_THICKNESS / 2;
}
setX(x: number): this {
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];
mod.setX(mod.getX() + shift);
}
return this;
}
setWidth(width: number): this {
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(): number {
return this.width;
}
getStyle(): ElementStyle {
return {
fillStyle: this.options.fill_style,
strokeStyle: this.options.fill_style, // yes, this is correct for legacy compatibility
lineWidth: Tables.STAVE_LINE_THICKNESS,
...super.getStyle(),
};
}
/**
* Set the measure number of this Stave.
*/
setMeasure(measure: number): this {
this.measure = measure;
return this;
}
/**
* Return the measure number of this Stave.
*/
getMeasure(): number {
return this.measure;
}
/**
* 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: number = 0): number {
if (typeof index !== 'number') {
throw new RuntimeError('InvalidIndex', 'Must be of number type');
}
if (!this.formatted) this.format();
if (this.getModifiers(StaveModifierPosition.BEGIN).length === 1) {
return 0;
}
// for right position modifiers zero shift seems correct, see 'Volta + Modifier Measure Test'
if (this.modifiers[index].getPosition() === StaveModifierPosition.RIGHT) {
return 0;
}
let start_x = this.start_x - this.x;
const begBarline = this.modifiers[0] as Barline;
if (begBarline.getType() === BarlineType.REPEAT_BEGIN && start_x > begBarline.getWidth()) {
start_x -= begBarline.getWidth();
}
return start_x;
}
/** Coda & Segno Symbol functions */
setRepetitionType(type: number, yShift: number = 0): this {
this.modifiers.push(new Repetition(type, this.x, yShift));
return this;
}
// Volta functions
setVoltaType(type: number, number_t: string, y: number): this {
this.modifiers.push(new Volta(type, number_t, this.x, y));
return this;
}
// Section functions
setSection(section: string, y: number, xOffset = 0, fontSize?: number, drawRect = true) {
const staveSection = new StaveSection(section, this.x + xOffset, y, drawRect);
if (fontSize) staveSection.setFontSize(fontSize);
this.modifiers.push(staveSection);
return this;
}
// Tempo functions
setTempo(tempo: StaveTempoOptions, y: number): this {
this.modifiers.push(new StaveTempo(tempo, this.x, y));
return this;
}
// Text functions
setText(
text: string,
position: number,
options: {
shift_x?: number;
shift_y?: number;
justification?: number;
} = {}
): this {
this.modifiers.push(new StaveText(text, position, options));
return this;
}
getHeight(): number {
return this.height;
}
getSpacingBetweenLines(): number {
return this.options.spacing_between_lines_px;
}
getBoundingBox(): BoundingBox {
return new BoundingBox(this.x, this.y, this.width, this.getBottomY() - this.y);
}
getBottomY(): number {
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(): number {
return this.getYForLine(this.options.num_lines);
}
// This returns
/** @returns the y for the *center* of a staff line */
getYForLine(line: number): number {
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: number): number {
// 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: number = 0): number {
return this.getYForLine(-line - this.options.top_text_position);
}
getYForBottomText(line: number = 0): number {
return this.getYForLine(this.options.bottom_text_position + line);
}
getYForNote(line: number): number {
const options = this.options;
const spacing = options.spacing_between_lines_px;
const headroom = options.space_above_staff_ln;
return this.y + headroom * spacing + 5 * spacing - line * spacing;
}
getYForGlyphs(): number {
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: StaveModifier, position?: number): this {
if (position !== undefined) {
modifier.setPosition(position);
}
modifier.setStave(this);
this.formatted = false;
this.modifiers.push(modifier);
return this;
}
addEndModifier(modifier: StaveModifier): this {
this.addModifier(modifier, StaveModifierPosition.END);
return this;
}
// Bar Line functions
setBegBarType(type: number | BarlineType): this {
// Only valid bar types at beginning of stave is none, single or begin repeat
const { SINGLE, REPEAT_BEGIN, NONE } = BarlineType;
if (type === SINGLE || type === REPEAT_BEGIN || type === NONE) {
(this.modifiers[0] as Barline).setType(type);
this.formatted = false;
}
return this;
}
setEndBarType(type: number | BarlineType): this {
// Repeat end not valid at end of stave
if (type !== BarlineType.REPEAT_BEGIN) {
(this.modifiers[1] as Barline).setType(type);
this.formatted = false;
}
return this;
}
setClef(clefSpec: string, size?: string, annotation?: string, position?: number): this {
if (position === undefined) {
position = StaveModifierPosition.BEGIN;
}
if (position === StaveModifierPosition.END) {
this.endClef = clefSpec;
} else {
this.clef = clefSpec;
}
const clefs = this.getModifiers(position, Clef.CATEGORY) as Clef[];
if (clefs.length === 0) {
this.addClef(clefSpec, size, annotation, position);
} else {
clefs[0].setType(clefSpec, size, annotation);
}
return this;
}
getClef(): string {
return this.clef;
}
setEndClef(clefSpec: string, size?: string, annotation?: string): this {
this.setClef(clefSpec, size, annotation, StaveModifierPosition.END);
return this;
}
getEndClef(): string | undefined {
return this.endClef;
}
setKeySignature(keySpec: string, cancelKeySpec?: string, position?: number): this {
if (position === undefined) {
position = StaveModifierPosition.BEGIN;
}
const keySignatures = this.getModifiers(position, KeySignature.CATEGORY) as KeySignature[];
if (keySignatures.length === 0) {
this.addKeySignature(keySpec, cancelKeySpec, position);
} else {
keySignatures[0].setKeySig(keySpec, cancelKeySpec);
}
return this;
}
setEndKeySignature(keySpec: string, cancelKeySpec?: string): this {
this.setKeySignature(keySpec, cancelKeySpec, StaveModifierPosition.END);
return this;
}
setTimeSignature(timeSpec: string, customPadding?: number, position?: number): this {
if (position === undefined) {
position = StaveModifierPosition.BEGIN;
}
const timeSignatures = this.getModifiers(position, TimeSignature.CATEGORY) as TimeSignature[];
if (timeSignatures.length === 0) {
this.addTimeSignature(timeSpec, customPadding, position);
} else {
timeSignatures[0].setTimeSig(timeSpec);
}
return this;
}
setEndTimeSignature(timeSpec: string, customPadding?: number): this {
this.setTimeSignature(timeSpec, customPadding, StaveModifierPosition.END);
return this;
}
/**
* Add a key signature to the stave.
*
* Example:
* `stave.addKeySignature('Db');`
* @param keySpec new key specification `[A-G][b|#]?`
* @param cancelKeySpec
* @param position
* @returns
*/
addKeySignature(keySpec: string, cancelKeySpec?: string, position?: number): this {
if (position === undefined) {
position = StaveModifierPosition.BEGIN;
}
this.addModifier(new KeySignature(keySpec, cancelKeySpec).setPosition(position), position);
return this;
}
/**
* Add a clef to the stave.
*
* Example:
*
* stave.addClef('treble')
* @param clef clef (treble|bass|...) see {@link Clef.types}
* @param size
* @param annotation
* @param position
* @returns
*/
addClef(clef: string, size?: string, annotation?: string, position?: number): this {
if (position === undefined || position === StaveModifierPosition.BEGIN) {
this.clef = clef;
} else if (position === StaveModifierPosition.END) {
this.endClef = clef;
}
this.addModifier(new Clef(clef, size, annotation), position);
return this;
}
addEndClef(clef: string, size?: string, annotation?: string): this {
this.addClef(clef, size, annotation, StaveModifierPosition.END);
return this;
}
/**
* Add a time signature to the stave
*
* Example:
*
* `stave.addTimeSignature('4/4');`
* @param timeSpec time signature specification `(C\||C|\d\/\d)`
* @param customPadding
* @param position
* @returns
*/
addTimeSignature(timeSpec: string, customPadding?: number, position?: number): this {
this.addModifier(new TimeSignature(timeSpec, customPadding), position);
return this;
}
addEndTimeSignature(timeSpec: string, customPadding?: number): this {
this.addTimeSignature(timeSpec, customPadding, StaveModifierPosition.END);
return this;
}
// Deprecated
addTrebleGlyph(): this {
this.addClef('treble');
return this;
}
/**
* @param position
* @param category
* @returns array of StaveModifiers that match the provided position and category.
*/
getModifiers(position?: number, category?: string): StaveModifier[] {
const noPosition = position === undefined;
const noCategory = category === undefined;
if (noPosition && noCategory) {
return this.modifiers;
} else if (noPosition) {
// A category was provided.
return this.modifiers.filter((m: StaveModifier) => category === m.getCategory());
} else if (noCategory) {
// A position was provided.
return this.modifiers.filter((m: StaveModifier) => position === m.getPosition());
} else {
// Both position and category were provided!
return this.modifiers.filter((m: StaveModifier) => position === m.getPosition() && category === m.getCategory());
}
}
/**
* Use the modifier's `getCategory()` as a key for the `order` array.
* The retrieved value is used to sort modifiers from left to right (0 to to 3).
*/
sortByCategory(items: StaveModifier[], order: Record<string, number>): void {
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(): void {
const begBarline = this.modifiers[0] as Barline;
const endBarline = this.modifiers[1];
const begModifiers = this.getModifiers(StaveModifierPosition.BEGIN);
const endModifiers = this.getModifiers(StaveModifierPosition.END);
this.sortByCategory(begModifiers, SORT_ORDER_BEG_MODIFIERS);
this.sortByCategory(endModifiers, SORT_ORDER_END_MODIFIERS);
if (begModifiers.length > 1 && begBarline.getType() === BarlineType.REPEAT_BEGIN) {
begModifiers.push(begModifiers.splice(0, 1)[0]);
begModifiers.splice(0, 0, new Barline(BarlineType.SINGLE));
}
if (endModifiers.indexOf(endBarline) > 0) {
endModifiers.splice(0, 0, new Barline(BarlineType.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();
x += padding;
modifier.setX(x);
x += width;
if (padding + width === 0) offset--;
}
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 = isBarline(modifier) ? 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 {
const ctx = this.checkContext();
this.setRendered();
this.applyStyle();
ctx.openGroup('stave', this.getAttribute('id'));
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);
if (this.options.line_config[line].visible) {
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.stroke();
}
}
ctx.closeGroup();
this.restoreStyle();
// Draw the modifiers (bar lines, coda, segno, repeat brackets, etc.)
for (let i = 0; i < this.modifiers.length; i++) {
const modifier = this.modifiers[i];
// Only draw modifier if it has a draw function
if (typeof modifier.draw === 'function') {
modifier.applyStyle(ctx);
modifier.draw(this, this.getModifierXShift(i));
modifier.restoreStyle(ctx);
}
}
// Render measure numbers
if (this.measure > 0) {
ctx.save();
ctx.setFont(this.textFont);
const textWidth = ctx.measureText('' + this.measure).width;
y = this.getYForTopText(0) + 3;
ctx.fillText('' + this.measure, this.x - textWidth / 2, y);
ctx.restore();
}
return this;
}
getVerticalBarWidth(): number {
return this.options.vertical_bar_width;
}
/**
* Get the current configuration for the Stave.
* @return {Array} An array of configuration objects.
*/
getConfigForLines(): StaveLineConfig[] {
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 RuntimeError "StaveConfigError" When the specified line number is out of
* range of the number of lines specified in the constructor.
*/
setConfigForLine(line_number: number, line_config: StaveLineConfig): this {
if (line_number >= this.options.num_lines || line_number < 0) {
throw new RuntimeError(
'StaveConfigError',
'The line number must be within the range of the number of lines in the Stave.'
);
}
if (line_config.visible === undefined) {
throw new RuntimeError('StaveConfigError', "The line configuration object is missing the 'visible' property.");
}
if (typeof line_config.visible !== 'boolean') {
throw new RuntimeError(
'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 RuntimeError "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: StaveLineConfig[]): this {
if (lines_configuration.length !== this.options.num_lines) {
throw new RuntimeError(
'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 '{}' to be used if the caller just wants the default for a particular node.
if (lines_configuration[line_config].visible == undefined) {
lines_configuration[line_config] = this.options.line_config[line_config];
}
this.options.line_config[line_config] = {
...this.options.line_config[line_config],
...lines_configuration[line_config],
};
}
this.options.line_config = lines_configuration;
return this;
}
static formatBegModifiers(staves: Stave[]): void {
const adjustCategoryStartX = (category: Category) => {
let minStartX = 0;
// Calculate min start X for the category
staves.forEach((stave) => {
const modifiers = stave.getModifiers(StaveModifierPosition.BEGIN, category);
// Consider only the first instance
if (modifiers.length > 0 && modifiers[0].getX() > minStartX) minStartX = modifiers[0].getX();
});
let adjustX = 0;
staves.forEach((stave) => {
adjustX = 0;
const modifiers = stave.getModifiers(StaveModifierPosition.BEGIN, category);
// Calculate adjustement required for the stave
modifiers.forEach((modifier) => {
if (minStartX - modifier.getX() > adjustX) adjustX = minStartX - modifier.getX();
});
const allModifiers = stave.getModifiers(StaveModifierPosition.BEGIN);
let bAdjust = false;
// Apply adjustment to all the modifiers in and beyond the category
allModifiers.forEach((modifier) => {
if (modifier.getCategory() === category) bAdjust = true;
if (bAdjust && adjustX > 0) modifier.setX(modifier.getX() + adjustX);
});
// Apply adjustment also to note start.
stave.setNoteStartX(stave.getNoteStartX() + adjustX);
});
};
// Make sure that staves are formatted
staves.forEach((stave) => {
if (!stave.formatted) stave.format();
});
// Align Clefs
adjustCategoryStartX(Category.Clef);
// Align key signatures
adjustCategoryStartX(Category.KeySignature);
// Align time signatures
adjustCategoryStartX(Category.TimeSignature);
let maxX = 0;
// align note start
staves.forEach((stave) => {
if (stave.getNoteStartX() > maxX) maxX = stave.getNoteStartX();
});
staves.forEach((stave) => {
stave.setNoteStartX(maxX);
});
maxX = 0;
// align REPEAT_BEGIN
staves.forEach((stave) => {
const modifiers = stave.getModifiers(StaveModifierPosition.BEGIN, Category.Barline);
modifiers.forEach((modifier) => {
if ((modifier as Barline).getType() == BarlineType.REPEAT_BEGIN)
if (modifier.getX() > maxX) maxX = modifier.getX();
});
});
staves.forEach((stave) => {
const modifiers = stave.getModifiers(StaveModifierPosition.BEGIN, Category.Barline);
modifiers.forEach((modifier) => {
if ((modifier as Barline).getType() == BarlineType.REPEAT_BEGIN) modifier.setX(maxX);
});
});
}
}