Skip to content

Commit 7ea8d9e

Browse files
authored
Improve Beaming algorithm to handle collisions (#491)
1 parent 968764a commit 7ea8d9e

File tree

18 files changed

+309
-106
lines changed

18 files changed

+309
-106
lines changed

src.compiler/csharp/CSharpAstTransformer.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2526,12 +2526,20 @@ export default class CSharpAstTransformer {
25262526
expression: {} as cs.Expression
25272527
} as cs.InvocationExpression;
25282528

2529+
const parts = expression.text.split('/');
25292530
csExpr.expression = this.makeMemberAccess(csExpr, 'AlphaTab.Core.TypeHelper', 'CreateRegex');
25302531
csExpr.arguments.push({
25312532
parent: csExpr,
25322533
nodeType: cs.SyntaxKind.StringLiteral,
25332534
tsNode: expression,
2534-
text: expression.text
2535+
text: parts[1]
2536+
} as cs.StringLiteral);
2537+
2538+
csExpr.arguments.push({
2539+
parent: csExpr,
2540+
nodeType: cs.SyntaxKind.StringLiteral,
2541+
tsNode: expression,
2542+
text: parts[2]
25352543
} as cs.StringLiteral);
25362544

25372545
return csExpr;
Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,57 @@
1-
using System.Text.RegularExpressions;
1+
using System.Collections.Concurrent;
2+
using System.Text.RegularExpressions;
23

34
namespace AlphaTab.Core.EcmaScript
45
{
56
public class RegExp
67
{
8+
private static ConcurrentDictionary<(string pattern, string flags), RegExp> Cache =
9+
new ConcurrentDictionary<(string pattern, string flags), RegExp>();
10+
711
private readonly Regex _regex;
12+
private readonly bool _global;
813

9-
public RegExp(string regex)
14+
public RegExp(string regex, string flags = "")
1015
{
11-
_regex = new Regex(regex, RegexOptions.Compiled);
16+
if (!Cache.TryGetValue((regex, flags), out var cached))
17+
{
18+
var netFlags = RegexOptions.Compiled;
19+
foreach (var c in flags)
20+
{
21+
switch (c)
22+
{
23+
case 'i':
24+
netFlags |= RegexOptions.IgnoreCase;
25+
break;
26+
case 'g':
27+
_global = true;
28+
break;
29+
case 'm':
30+
netFlags |= RegexOptions.Multiline;
31+
break;
32+
}
33+
}
34+
35+
_regex = new Regex(regex, netFlags);
36+
Cache[(regex, flags)] = this;
37+
}
38+
else
39+
{
40+
_regex = cached._regex;
41+
_global = cached._global;
42+
}
1243
}
1344

1445
public bool Exec(string s)
1546
{
1647
return _regex.IsMatch(s);
1748
}
49+
50+
public string Replace(string input, string replacement)
51+
{
52+
return _global
53+
? _regex.Replace(input, replacement)
54+
: _regex.Replace(input, replacement, 1);
55+
}
1856
}
1957
}

src.csharp/AlphaTab/Core/TypeHelper.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Diagnostics.CodeAnalysis;
43
using System.Globalization;
54
using System.Linq;
65
using System.Runtime.CompilerServices;
76
using AlphaTab.Core.EcmaScript;
8-
using AlphaTab.Rendering.Glyphs;
9-
using String = System.String;
107

118
namespace AlphaTab.Core
129
{
@@ -273,6 +270,18 @@ public static string ToString(this double num, int radix)
273270
return num.ToString(CultureInfo.InvariantCulture);
274271
}
275272

273+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
274+
public static RegExp CreateRegex(string pattern, string flags)
275+
{
276+
return new RegExp(pattern, flags);
277+
}
278+
279+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
280+
public static string Replace(this string input, RegExp pattern, string replacement)
281+
{
282+
return pattern.Replace(input, replacement);
283+
}
284+
276285
[MethodImpl(MethodImplOptions.AggressiveInlining)]
277286
public static bool IsTruthy(string? s)
278287
{

src/platform/svg/SvgCanvas.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ export abstract class SvgCanvas implements ICanvas {
2222
public settings!: Settings;
2323

2424
public beginRender(width: number, height: number): void {
25-
this.buffer = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width | 0}px" height="${
26-
height | 0
27-
}px" class="at-surface-svg">\n`;
25+
this.buffer = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width | 0}px" height="${height | 0
26+
}px" class="at-surface-svg">\n`;
2827
this._currentPath = '';
2928
this._currentPathIsEmpty = true;
3029
}
@@ -137,10 +136,19 @@ export abstract class SvgCanvas implements ICanvas {
137136
if (this.textAlign !== TextAlign.Left) {
138137
s += ` text-anchor="${this.getSvgTextAlignment(this.textAlign)}"`;
139138
}
140-
s += `>${text}</text>`;
139+
s += `>${SvgCanvas.escapeText(text)}</text>`;
141140
this.buffer += s;
142141
}
143142

143+
private static escapeText(text: string) {
144+
return text
145+
.replace(/&/g, '&amp;')
146+
.replace(/"/g, '&quot;')
147+
.replace(/'/g, '&#39;')
148+
.replace(/</g, '&lt;')
149+
.replace(/>/g, '&gt;');
150+
}
151+
144152
protected getSvgTextAlignment(textAlign: TextAlign): string {
145153
switch (textAlign) {
146154
case TextAlign.Left:

src/rendering/ScoreBarRenderer.ts

Lines changed: 92 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { ScoreBeatContainerGlyph } from '@src/rendering/ScoreBeatContainerGlyph'
2929
import { ScoreRenderer } from '@src/rendering/ScoreRenderer';
3030
import { AccidentalHelper } from '@src/rendering/utils/AccidentalHelper';
3131
import { BeamDirection } from '@src/rendering/utils/BeamDirection';
32-
import { BeamingHelper } from '@src/rendering/utils/BeamingHelper';
32+
import { BeamingHelper, BeamingHelperDrawInfo } from '@src/rendering/utils/BeamingHelper';
3333
import { RenderingResources } from '@src/RenderingResources';
3434
import { Settings } from '@src/Settings';
3535
import { ModelUtils } from '@src/model/ModelUtils';
@@ -354,71 +354,104 @@ export class ScoreBarRenderer extends BarRendererBase {
354354
private calculateBeamYWithDirection(h: BeamingHelper, x: number, direction: BeamDirection): number {
355355
let stemSize: number = this.getStemSize(h);
356356

357-
const firstBeat = h.beats[0];
357+
if (!h.drawingInfos.has(direction)) {
358+
let drawingInfo = new BeamingHelperDrawInfo();
359+
h.drawingInfos.set(direction, drawingInfo);
358360

359-
// create a line between the min and max note of the group
360-
if (h.beats.length === 1) {
361-
if (direction === BeamDirection.Up) {
362-
return this.getScoreY(this.accidentalHelper.getMinLine(firstBeat)) - stemSize;
363-
}
364-
return this.getScoreY(this.accidentalHelper.getMaxLine(firstBeat)) + stemSize;
365-
}
361+
// the beaming logic works like this:
362+
// 1. we take the first and last note, add the stem, and put a diagnal line between them.
363+
// 2. the height of the diagonal line must not exceed a max height,
364+
// - if this is the case, the line on the more distant note just gets longer
365+
// 3. any middle elements (notes or rests) shift this diagonal line up/down to avoid overlaps
366366

367-
const lastBeat = h.beats[h.beats.length - 1];
367+
const firstBeat = h.beats[0];
368+
const lastBeat = h.beats[h.beats.length - 1];
368369

369-
// we use the min/max notes to place the beam along their real position
370-
// we only want a maximum of 10 offset for their gradient
371-
let maxDistance: number = 10 * this.scale;
372-
// if the min note is not first or last, we can align notes directly to the position
373-
// of the min note
374-
const beatOfLowestNote = h.beatOfLowestNote;
375-
const beatOfHighestNote = h.beatOfHighestNote;
376-
if (
377-
direction === BeamDirection.Down &&
378-
beatOfLowestNote !== firstBeat &&
379-
beatOfLowestNote !== lastBeat
380-
) {
381-
return this.getScoreY(this.accidentalHelper.getMaxLine(beatOfLowestNote)) + stemSize;
382-
}
383-
if (
384-
direction === BeamDirection.Up &&
385-
beatOfHighestNote !== firstBeat &&
386-
beatOfHighestNote !== lastBeat
387-
) {
388-
return this.getScoreY(this.accidentalHelper.getMinLine(beatOfHighestNote)) - stemSize;
389-
}
370+
// 1. put direct diagonal line.
371+
drawingInfo.startX = h.getBeatLineX(firstBeat);
372+
drawingInfo.startY =
373+
direction === BeamDirection.Up
374+
? this.getScoreY(this.accidentalHelper.getMinLine(firstBeat)) - stemSize
375+
: this.getScoreY(this.accidentalHelper.getMaxLine(firstBeat)) + stemSize;
390376

391-
let startX: number = h.getBeatLineX(firstBeat);
392-
let startY: number =
393-
direction === BeamDirection.Up
394-
? this.getScoreY(this.accidentalHelper.getMinLine(firstBeat)) - stemSize
395-
: this.getScoreY(this.accidentalHelper.getMaxLine(firstBeat)) + stemSize;
377+
drawingInfo.endX = h.getBeatLineX(lastBeat);
378+
drawingInfo.endY =
379+
direction === BeamDirection.Up
380+
? this.getScoreY(this.accidentalHelper.getMinLine(lastBeat)) - stemSize
381+
: this.getScoreY(this.accidentalHelper.getMaxLine(lastBeat)) + stemSize;
382+
383+
// 2. ensure max height
384+
// we use the min/max notes to place the beam along their real position
385+
// we only want a maximum of 10 offset for their gradient
386+
let maxDistance: number = 10 * this.scale;
387+
if (direction === BeamDirection.Down && drawingInfo.startY > drawingInfo.endY && drawingInfo.startY - drawingInfo.endY > maxDistance) {
388+
drawingInfo.endY = drawingInfo.startY - maxDistance;
389+
}
390+
if (direction === BeamDirection.Down && drawingInfo.endY > drawingInfo.startY && drawingInfo.endY - drawingInfo.startY > maxDistance) {
391+
drawingInfo.startY = drawingInfo.endY - maxDistance;
392+
}
393+
if (direction === BeamDirection.Up && drawingInfo.startY < drawingInfo.endY && drawingInfo.endY - drawingInfo.startY > maxDistance) {
394+
drawingInfo.endY = drawingInfo.startY + maxDistance;
395+
}
396+
if (direction === BeamDirection.Up && drawingInfo.endY < drawingInfo.startY && drawingInfo.startY - drawingInfo.endY > maxDistance) {
397+
drawingInfo.startY = drawingInfo.endY + maxDistance;
398+
}
396399

397-
let endX: number = h.getBeatLineX(lastBeat);
398-
let endY: number =
399-
direction === BeamDirection.Up
400-
? this.getScoreY(this.accidentalHelper.getMinLine(lastBeat)) - stemSize
401-
: this.getScoreY(this.accidentalHelper.getMaxLine(lastBeat)) + stemSize;
400+
// 3. let middle elements shift up/down
401+
if (h.beats.length > 1) {
402+
// check if highest note shifts bar up or down
403+
if (direction === BeamDirection.Up) {
404+
let yNeededForHighestNote = this.getScoreY(this.accidentalHelper.getMinLine(h.beatOfHighestNote)) - stemSize;
405+
const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfHighestNote));
406+
407+
const diff = yGivenByCurrentValues - yNeededForHighestNote;
408+
if (diff > 0) {
409+
drawingInfo.startY -= diff;
410+
drawingInfo.endY -= diff;
411+
}
412+
} else {
413+
let yNeededForLowestNote = this.getScoreY(this.accidentalHelper.getMaxLine(h.beatOfLowestNote)) + stemSize;
414+
const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfLowestNote));
415+
416+
const diff = yNeededForLowestNote - yGivenByCurrentValues;
417+
if (diff > 0) {
418+
drawingInfo.startY += diff;
419+
drawingInfo.endY += diff;
420+
}
421+
}
402422

403-
// ensure the maxDistance
404-
if (direction === BeamDirection.Down && startY > endY && startY - endY > maxDistance) {
405-
endY = startY - maxDistance;
406-
}
407-
if (direction === BeamDirection.Down && endY > startY && endY - startY > maxDistance) {
408-
startY = endY - maxDistance;
409-
}
410-
if (direction === BeamDirection.Up && startY < endY && endY - startY > maxDistance) {
411-
endY = startY + maxDistance;
412-
}
413-
if (direction === BeamDirection.Up && endY < startY && startY - endY > maxDistance) {
414-
startY = endY + maxDistance;
415-
}
416-
// get the y position of the given beat on this curve
417-
if (startX === endX) {
418-
return startY;
423+
// check if rest shifts bar up or down
424+
if (h.minRestLine !== null || h.maxRestLine !== null) {
425+
const barCount: number = ModelUtils.getIndex(h.shortestDuration) - 2;
426+
let scaleMod: number = h.isGrace ? NoteHeadGlyph.GraceScale : 1;
427+
let barSpacing: number = barCount *
428+
(BarRendererBase.BeamSpacing + BarRendererBase.BeamThickness) * this.scale * scaleMod;
429+
barSpacing += BarRendererBase.BeamSpacing;
430+
431+
if (direction === BeamDirection.Up && h.minRestLine !== null) {
432+
let yNeededForRest = this.getScoreY(h.minRestLine!) - barSpacing;
433+
const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfMinRestLine!));
434+
435+
const diff = yGivenByCurrentValues - yNeededForRest;
436+
if (diff > 0) {
437+
drawingInfo.startY -= diff;
438+
drawingInfo.endY -= diff;
439+
}
440+
} else if (direction === BeamDirection.Down && h.maxRestLine !== null) {
441+
let yNeededForRest = this.getScoreY(h.maxRestLine!) + barSpacing;
442+
const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfMaxRestLine!));
443+
444+
const diff = yNeededForRest - yGivenByCurrentValues;
445+
if (diff > 0) {
446+
drawingInfo.startY += diff;
447+
drawingInfo.endY += diff;
448+
}
449+
}
450+
}
451+
}
419452
}
420-
// y(x) = ( (y2 - y1) / (x2 - x1) ) * (x - x1) + y1;
421-
return ((endY - startY) / (endX - startX)) * (x - startX) + startY;
453+
454+
return h.drawingInfos.get(direction)!.calcY(x);
422455
}
423456

424457

src/rendering/glyphs/ScoreBeatGlyph.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase {
8282
0,
8383
0,
8484
4 *
85-
(this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) *
86-
this.scale
85+
(this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) *
86+
this.scale
8787
)
8888
);
8989
this.addGlyph(ghost);
@@ -129,6 +129,10 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase {
129129
this.restGlyph.beat = this.container.beat;
130130
this.restGlyph.beamingHelper = this.beamingHelper;
131131
this.addGlyph(this.restGlyph);
132+
if (this.beamingHelper) {
133+
this.beamingHelper.applyRest(this.container.beat, line);
134+
}
135+
132136
//
133137
// Note dots
134138
//

src/rendering/utils/AccidentalHelper.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ export class AccidentalHelper {
265265
const previousRenderer = this._barRenderer.previousRenderer as ScoreBarRenderer;
266266
if (previousRenderer) {
267267
const tieOriginLine = previousRenderer.accidentalHelper.getNoteLine(note.tieOrigin!);
268-
if(tieOriginLine === line) {
268+
if (tieOriginLine === line) {
269269
skipAccidental = true;
270270
}
271271
}
@@ -328,26 +328,29 @@ export class AccidentalHelper {
328328
}
329329

330330
if (!isHelperNote) {
331-
let lines: BeatLines;
332-
if (this._beatLines.has(relatedBeat.id)) {
333-
lines = this._beatLines.get(relatedBeat.id)!;
334-
}
335-
else {
336-
lines = new BeatLines();
337-
this._beatLines.set(relatedBeat.id, lines);
338-
}
339-
340-
if (lines.minLine === -1000 || line < lines.minLine) {
341-
lines.minLine = line;
342-
}
343-
if (lines.minLine === -1000 || line > lines.maxLine) {
344-
lines.maxLine = line;
345-
}
331+
this.registerLine(relatedBeat, line);
346332
}
347333

348334
return accidentalToSet;
349335
}
350336

337+
private registerLine(relatedBeat: Beat, line: number) {
338+
let lines: BeatLines;
339+
if (this._beatLines.has(relatedBeat.id)) {
340+
lines = this._beatLines.get(relatedBeat.id)!;
341+
}
342+
else {
343+
lines = new BeatLines();
344+
this._beatLines.set(relatedBeat.id, lines);
345+
}
346+
if (lines.minLine === -1000 || line < lines.minLine) {
347+
lines.minLine = line;
348+
}
349+
if (lines.minLine === -1000 || line > lines.maxLine) {
350+
lines.maxLine = line;
351+
}
352+
}
353+
351354
public getMaxLine(b: Beat): number {
352355
return this._beatLines.has(b.id)
353356
? this._beatLines.get(b.id)!.maxLine

0 commit comments

Comments
 (0)