Skip to content

Commit 2a2fe44

Browse files
authored
Merge pull request #1580 from gristow/two-note-chord-spacing
fix: extra space before seconds with modifiers
2 parents 625ab61 + c32f936 commit 2a2fe44

File tree

3 files changed

+142
-63
lines changed

3 files changed

+142
-63
lines changed

src/accidental.ts

Lines changed: 101 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,18 @@ import { Category, isAccidental, isGraceNote, isGraceNoteGroup, isStaveNote } fr
1515
import { defined, log } from './util';
1616
import { Voice } from './voice';
1717

18-
export type Line = {
18+
type StaveLineAccidentalLayoutMetrics = {
1919
column: number;
2020
line: number;
21+
/**
22+
* A flat line needs more clearance above than below. This is
23+
* set to true if the accidental is either a flat or double flat.
24+
*/
2125
flatLine: boolean;
26+
/**
27+
* Double sharps need less clearance above and below than other
28+
* accidentals.
29+
*/
2230
dblSharpLine: boolean;
2331
numAcc: number;
2432
width: number;
@@ -74,17 +82,21 @@ export class Accidental extends Modifier {
7482
const additionalPadding = musicFont.lookupMetric('accidental.leftPadding'); // padding to the left of all accidentals
7583

7684
// A type used just in this formatting function.
77-
type AccidentalListItem = {
85+
type AccidentalLinePositionsAndXSpaceNeeds = {
7886
y?: number;
7987
line: number;
80-
shift: number;
88+
/**
89+
* The amount by which the accidental requests notes be shifted to the right
90+
* to accomodate its presence.
91+
*/
92+
extraXSpaceNeeded: number;
8193
acc: Accidental;
82-
lineSpace?: number;
94+
spacingBetweenStaveLines?: number;
8395
};
8496

85-
const accList: AccidentalListItem[] = [];
97+
const accidentalLinePositionsAndSpaceNeeds: AccidentalLinePositionsAndXSpaceNeeds[] = [];
8698
let prevNote = undefined;
87-
let shiftL = 0;
99+
let extraXSpaceNeededForLeftDisplacedNotehead = 0;
88100

89101
// First determine the accidentals' Y positions from the note.keys
90102
for (let i = 0; i < accidentals.length; ++i) {
@@ -94,75 +106,97 @@ export class Accidental extends Modifier {
94106
const stave = note.getStave();
95107
const index = acc.checkIndex();
96108
const props = note.getKeyProps()[index];
109+
97110
if (note !== prevNote) {
98111
// Iterate through all notes to get the displaced pixels
99112
for (let n = 0; n < note.keys.length; ++n) {
100-
shiftL = Math.max(note.getLeftDisplacedHeadPx() - note.getXShift(), shiftL);
113+
// If the current extra left-space needed isn't as big as this note's,
114+
// then we need to use this note's.
115+
extraXSpaceNeededForLeftDisplacedNotehead = Math.max(
116+
note.getLeftDisplacedHeadPx() - note.getXShift(),
117+
extraXSpaceNeededForLeftDisplacedNotehead
118+
);
101119
}
102120
prevNote = note;
103121
}
104122
if (stave) {
105123
const lineSpace = stave.getSpacingBetweenLines();
106124
const y = stave.getYForLine(props.line);
107125
const accLine = Math.round((y / lineSpace) * 2) / 2;
108-
accList.push({ y, line: accLine, shift: shiftL, acc, lineSpace });
126+
accidentalLinePositionsAndSpaceNeeds.push({
127+
y,
128+
line: accLine,
129+
extraXSpaceNeeded: extraXSpaceNeededForLeftDisplacedNotehead,
130+
acc,
131+
spacingBetweenStaveLines: lineSpace,
132+
});
109133
} else {
110-
accList.push({ line: props.line, shift: shiftL, acc });
134+
accidentalLinePositionsAndSpaceNeeds.push({
135+
line: props.line,
136+
extraXSpaceNeeded: extraXSpaceNeededForLeftDisplacedNotehead,
137+
acc,
138+
});
111139
}
112140
}
113141

114142
// Sort accidentals by line number.
115-
accList.sort((a, b) => b.line - a.line);
143+
accidentalLinePositionsAndSpaceNeeds.sort((a, b) => b.line - a.line);
116144

117-
// FIXME: Confusing name. Each object in this array has a property called `line`.
118-
// So if this is a list of lines, you end up with: `line.line` which is very awkward.
119-
const lineList: Line[] = [];
145+
const staveLineAccidentalLayoutMetrics: StaveLineAccidentalLayoutMetrics[] = [];
120146

121147
// amount by which all accidentals must be shifted right or left for
122148
// stem flipping, notehead shifting concerns.
123-
let accShift = 0;
124-
let previousLine = undefined;
149+
let maxExtraXSpaceNeeded = 0;
125150

126-
// Create an array of unique line numbers (lineList) from accList
127-
for (let i = 0; i < accList.length; i++) {
128-
const acc = accList[i];
151+
// Create an array of unique line numbers (staveLineAccidentalLayoutMetrics)
152+
// from accidentalLinePositionsAndSpaceNeeds
153+
for (let i = 0; i < accidentalLinePositionsAndSpaceNeeds.length; i++) {
154+
const accidentalLinePositionAndSpaceNeeds = accidentalLinePositionsAndSpaceNeeds[i];
129155

130-
// if this is the first line, or a new line, add a lineList
131-
if (previousLine === undefined || previousLine !== acc.line) {
132-
lineList.push({
133-
line: acc.line,
156+
const priorLineMetric = staveLineAccidentalLayoutMetrics[staveLineAccidentalLayoutMetrics.length - 1];
157+
let currentLineMetric: StaveLineAccidentalLayoutMetrics;
158+
159+
// if this is the first line, or a new line, add a staveLineAccidentalLayoutMetric
160+
if (!priorLineMetric || priorLineMetric?.line !== accidentalLinePositionAndSpaceNeeds.line) {
161+
currentLineMetric = {
162+
line: accidentalLinePositionAndSpaceNeeds.line,
134163
flatLine: true,
135164
dblSharpLine: true,
136165
numAcc: 0,
137166
width: 0,
138167
column: 0,
139-
});
168+
};
169+
staveLineAccidentalLayoutMetrics.push(currentLineMetric);
170+
} else {
171+
currentLineMetric = priorLineMetric;
140172
}
173+
141174
// if this accidental is not a flat, the accidental needs 3.0 lines lower
142175
// clearance instead of 2.5 lines for b or bb.
143-
// FIXME: Naming could use work. acc.acc is very awkward
144-
if (acc.acc.type !== 'b' && acc.acc.type !== 'bb') {
145-
lineList[lineList.length - 1].flatLine = false;
176+
if (
177+
accidentalLinePositionAndSpaceNeeds.acc.type !== 'b' &&
178+
accidentalLinePositionAndSpaceNeeds.acc.type !== 'bb'
179+
) {
180+
currentLineMetric.flatLine = false;
146181
}
147182

148183
// if this accidental is not a double sharp, the accidental needs 3.0 lines above
149-
if (acc.acc.type !== '##') {
150-
lineList[lineList.length - 1].dblSharpLine = false;
184+
if (accidentalLinePositionAndSpaceNeeds.acc.type !== '##') {
185+
currentLineMetric.dblSharpLine = false;
151186
}
152187

153188
// Track how many accidentals are on this line:
154-
lineList[lineList.length - 1].numAcc++;
189+
currentLineMetric.numAcc++;
155190

156191
// Track the total x_offset needed for this line which will be needed
157192
// for formatting lines w/ multiple accidentals:
158193

159194
// width = accidental width + universal spacing between accidentals
160-
lineList[lineList.length - 1].width += acc.acc.getWidth() + accidentalSpacing;
161-
162-
// if this accShift is larger, use it to keep first column accidentals in the same line
163-
accShift = acc.shift > accShift ? acc.shift : accShift;
195+
currentLineMetric.width += accidentalLinePositionAndSpaceNeeds.acc.getWidth() + accidentalSpacing;
164196

165-
previousLine = acc.line;
197+
// if this extraXSpaceNeeded is the largest so far, use it as the starting point for
198+
// all accidental columns.
199+
maxExtraXSpaceNeeded = Math.max(accidentalLinePositionAndSpaceNeeds.extraXSpaceNeeded, maxExtraXSpaceNeeded);
166200
}
167201

168202
// ### Place Accidentals in Columns
@@ -186,14 +220,19 @@ export class Accidental extends Modifier {
186220
let totalColumns = 0;
187221

188222
// establish the boundaries for a group of notes with clashing accidentals:
189-
for (let i = 0; i < lineList.length; i++) {
223+
for (let i = 0; i < staveLineAccidentalLayoutMetrics.length; i++) {
190224
let noFurtherConflicts = false;
191225
const groupStart = i;
192226
let groupEnd = i;
193227

194-
while (groupEnd + 1 < lineList.length && !noFurtherConflicts) {
228+
while (groupEnd + 1 < staveLineAccidentalLayoutMetrics.length && !noFurtherConflicts) {
195229
// if this note conflicts with the next:
196-
if (this.checkCollision(lineList[groupEnd], lineList[groupEnd + 1])) {
230+
if (
231+
this.checkCollision(
232+
staveLineAccidentalLayoutMetrics[groupEnd],
233+
staveLineAccidentalLayoutMetrics[groupEnd + 1]
234+
)
235+
) {
197236
// include the next note in the group:
198237
groupEnd++;
199238
} else {
@@ -202,7 +241,7 @@ export class Accidental extends Modifier {
202241
}
203242

204243
// Gets an a line from the `lineList`, relative to the current group
205-
const getGroupLine = (index: number) => lineList[groupStart + index];
244+
const getGroupLine = (index: number) => staveLineAccidentalLayoutMetrics[groupStart + index];
206245
const getGroupLines = (indexes: number[]) => indexes.map(getGroupLine);
207246
const lineDifference = (indexA: number, indexB: number) => {
208247
const [a, b] = getGroupLines([indexA, indexB]).map((item) => item.line);
@@ -216,7 +255,12 @@ export class Accidental extends Modifier {
216255
const groupLength = groupEnd - groupStart + 1;
217256

218257
// Set the accidental column for each line of the group
219-
let endCase = this.checkCollision(lineList[groupStart], lineList[groupEnd]) ? 'a' : 'b';
258+
let endCase = this.checkCollision(
259+
staveLineAccidentalLayoutMetrics[groupStart],
260+
staveLineAccidentalLayoutMetrics[groupEnd]
261+
)
262+
? 'a'
263+
: 'b';
220264

221265
switch (groupLength) {
222266
case 3:
@@ -259,8 +303,13 @@ export class Accidental extends Modifier {
259303
let collisionDetected = true;
260304
while (collisionDetected === true) {
261305
collisionDetected = false;
262-
for (let line = 0; line + patternLength < lineList.length; line++) {
263-
if (this.checkCollision(lineList[line], lineList[line + patternLength])) {
306+
for (let line = 0; line + patternLength < staveLineAccidentalLayoutMetrics.length; line++) {
307+
if (
308+
this.checkCollision(
309+
staveLineAccidentalLayoutMetrics[line],
310+
staveLineAccidentalLayoutMetrics[line + patternLength]
311+
)
312+
) {
264313
collisionDetected = true;
265314
patternLength++;
266315
break;
@@ -270,15 +319,15 @@ export class Accidental extends Modifier {
270319
// Then, assign a column to each line of accidentals
271320
for (groupMember = i; groupMember <= groupEnd; groupMember++) {
272321
column = ((groupMember - i) % patternLength) + 1;
273-
lineList[groupMember].column = column;
322+
staveLineAccidentalLayoutMetrics[groupMember].column = column;
274323
totalColumns = totalColumns > column ? totalColumns : column;
275324
}
276325
} else {
277326
// If the group contains fewer than seven members, use the layouts from
278327
// the Tables.accidentalColumnsTable (See: tables.ts).
279328
for (groupMember = i; groupMember <= groupEnd; groupMember++) {
280329
column = Tables.accidentalColumnsTable[groupLength][endCase][groupMember - i];
281-
lineList[groupMember].column = column;
330+
staveLineAccidentalLayoutMetrics[groupMember].column = column;
282331
totalColumns = totalColumns > column ? totalColumns : column;
283332
}
284333
}
@@ -308,12 +357,12 @@ export class Accidental extends Modifier {
308357
columnXOffsets[i] = 0;
309358
}
310359

311-
columnWidths[0] = accShift + leftShift;
312-
columnXOffsets[0] = accShift + leftShift;
360+
columnWidths[0] = leftShift + maxExtraXSpaceNeeded;
361+
columnXOffsets[0] = leftShift;
313362

314363
// Fill columnWidths with widest needed x-space;
315364
// this is what keeps the columns parallel.
316-
lineList.forEach((line) => {
365+
staveLineAccidentalLayoutMetrics.forEach((line) => {
317366
if (line.width > columnWidths[line.column]) columnWidths[line.column] = line.width;
318367
});
319368

@@ -325,26 +374,25 @@ export class Accidental extends Modifier {
325374
const totalShift = columnXOffsets[columnXOffsets.length - 1];
326375
// Set the xShift for each accidental according to column offsets:
327376
let accCount = 0;
328-
lineList.forEach((line) => {
377+
staveLineAccidentalLayoutMetrics.forEach((line) => {
329378
let lineWidth = 0;
330379
const lastAccOnLine = accCount + line.numAcc;
331380
// handle all of the accidentals on a given line:
332381
for (accCount; accCount < lastAccOnLine; accCount++) {
333-
const xShift = columnXOffsets[line.column - 1] + lineWidth;
334-
accList[accCount].acc.setXShift(xShift);
382+
const xShift = columnXOffsets[line.column - 1] + lineWidth + maxExtraXSpaceNeeded;
383+
accidentalLinePositionsAndSpaceNeeds[accCount].acc.setXShift(xShift);
335384
// keep track of the width of accidentals we've added so far, so that when
336385
// we loop, we add space for them.
337-
lineWidth += accList[accCount].acc.getWidth() + accidentalSpacing;
386+
lineWidth += accidentalLinePositionsAndSpaceNeeds[accCount].acc.getWidth() + accidentalSpacing;
338387
L('Line, accCount, shift: ', line.line, accCount, xShift);
339388
}
340389
});
341-
342390
// update the overall layout with the full width of the accidental shapes:
343-
state.left_shift += totalShift + additionalPadding;
391+
state.left_shift = totalShift + additionalPadding;
344392
}
345393

346394
/** Helper function to determine whether two lines of accidentals collide vertically */
347-
static checkCollision(line1: Line, line2: Line): boolean {
395+
static checkCollision(line1: StaveLineAccidentalLayoutMetrics, line2: StaveLineAccidentalLayoutMetrics): boolean {
348396
let clearance = line2.line - line1.line;
349397
let clearanceRequired = 3;
350398
// But less clearance is required for certain accidentals: b, bb and ##.

src/stringnumber.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,21 @@ export class StringNumber extends Modifier {
4848
// ## Static Methods
4949
// Arrange string numbers inside a `ModifierContext`
5050
static format(nums: StringNumber[], state: ModifierContextState): boolean {
51+
/**
52+
* The modifier context's left_shift state.
53+
*/
5154
const left_shift = state.left_shift;
55+
/**
56+
* The modifier context's right_shift state.
57+
*/
5258
const right_shift = state.right_shift;
5359
const num_spacing = 1;
5460

5561
if (!nums || nums.length === 0) return false;
5662

5763
const nums_list = [];
5864
let prev_note = null;
59-
let shift_left = 0;
65+
let extraXSpaceForDisplacedNotehead = 0;
6066
let shift_right = 0;
6167
const modLines = 0;
6268

@@ -84,8 +90,8 @@ export class StringNumber extends Modifier {
8490

8591
if (note !== prev_note) {
8692
for (let n = 0; n < note.keys.length; ++n) {
87-
if (left_shift === 0) {
88-
shift_left = Math.max(note.getLeftDisplacedHeadPx(), shift_left);
93+
if (pos === Modifier.Position.LEFT) {
94+
extraXSpaceForDisplacedNotehead = Math.max(note.getLeftDisplacedHeadPx(), extraXSpaceForDisplacedNotehead);
8995
}
9096
if (right_shift === 0) {
9197
shift_right = Math.max(note.getRightDisplacedHeadPx(), shift_right);
@@ -101,7 +107,7 @@ export class StringNumber extends Modifier {
101107
note,
102108
num,
103109
line: glyphLine,
104-
shiftL: shift_left,
110+
shiftL: extraXSpaceForDisplacedNotehead,
105111
shiftR: shift_right,
106112
});
107113
}
@@ -115,7 +121,6 @@ export class StringNumber extends Modifier {
115121
let last_line = null;
116122
let last_note = null;
117123
for (let i = 0; i < nums_list.length; ++i) {
118-
let num_shift = 0;
119124
const note = nums_list[i].note;
120125
const pos = nums_list[i].pos;
121126
const num = nums_list[i].num;
@@ -127,14 +132,15 @@ export class StringNumber extends Modifier {
127132
}
128133

129134
const num_width = num.getWidth() + num_spacing;
135+
let num_x_shift = 0;
130136
if (pos === Modifier.Position.LEFT) {
131-
num.setXShift(left_shift);
132-
num_shift = shift_left + num_width; // spacing
133-
x_widthL = num_shift > x_widthL ? num_shift : x_widthL;
137+
num.setXShift(left_shift + extraXSpaceForDisplacedNotehead);
138+
num_x_shift = num_width; // spacing
139+
x_widthL = Math.max(num_x_shift, x_widthL);
134140
} else if (pos === Modifier.Position.RIGHT) {
135141
num.setXShift(num_shiftR);
136-
num_shift += num_width; // spacing
137-
x_widthR = num_shift > x_widthR ? num_shift : x_widthR;
142+
num_x_shift += num_width; // spacing
143+
x_widthR = num_x_shift > x_widthR ? num_x_shift : x_widthR;
138144
}
139145
last_line = line;
140146
last_note = note;

0 commit comments

Comments
 (0)