Skip to content

Commit

Permalink
Improve Beaming algorithm to handle collisions (CoderLine#491)
Browse files Browse the repository at this point in the history
  • Loading branch information
Danielku15 authored Jan 3, 2021
1 parent 968764a commit 7ea8d9e
Show file tree
Hide file tree
Showing 18 changed files with 309 additions and 106 deletions.
10 changes: 9 additions & 1 deletion src.compiler/csharp/CSharpAstTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2526,12 +2526,20 @@ export default class CSharpAstTransformer {
expression: {} as cs.Expression
} as cs.InvocationExpression;

const parts = expression.text.split('/');
csExpr.expression = this.makeMemberAccess(csExpr, 'AlphaTab.Core.TypeHelper', 'CreateRegex');
csExpr.arguments.push({
parent: csExpr,
nodeType: cs.SyntaxKind.StringLiteral,
tsNode: expression,
text: expression.text
text: parts[1]
} as cs.StringLiteral);

csExpr.arguments.push({
parent: csExpr,
nodeType: cs.SyntaxKind.StringLiteral,
tsNode: expression,
text: parts[2]
} as cs.StringLiteral);

return csExpr;
Expand Down
44 changes: 41 additions & 3 deletions src.csharp/AlphaTab/Core/EcmaScript/RegExp.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
using System.Text.RegularExpressions;
using System.Collections.Concurrent;
using System.Text.RegularExpressions;

namespace AlphaTab.Core.EcmaScript
{
public class RegExp
{
private static ConcurrentDictionary<(string pattern, string flags), RegExp> Cache =
new ConcurrentDictionary<(string pattern, string flags), RegExp>();

private readonly Regex _regex;
private readonly bool _global;

public RegExp(string regex)
public RegExp(string regex, string flags = "")
{
_regex = new Regex(regex, RegexOptions.Compiled);
if (!Cache.TryGetValue((regex, flags), out var cached))
{
var netFlags = RegexOptions.Compiled;
foreach (var c in flags)
{
switch (c)
{
case 'i':
netFlags |= RegexOptions.IgnoreCase;
break;
case 'g':
_global = true;
break;
case 'm':
netFlags |= RegexOptions.Multiline;
break;
}
}

_regex = new Regex(regex, netFlags);
Cache[(regex, flags)] = this;
}
else
{
_regex = cached._regex;
_global = cached._global;
}
}

public bool Exec(string s)
{
return _regex.IsMatch(s);
}

public string Replace(string input, string replacement)
{
return _global
? _regex.Replace(input, replacement)
: _regex.Replace(input, replacement, 1);
}
}
}
15 changes: 12 additions & 3 deletions src.csharp/AlphaTab/Core/TypeHelper.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using AlphaTab.Core.EcmaScript;
using AlphaTab.Rendering.Glyphs;
using String = System.String;

namespace AlphaTab.Core
{
Expand Down Expand Up @@ -273,6 +270,18 @@ public static string ToString(this double num, int radix)
return num.ToString(CultureInfo.InvariantCulture);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RegExp CreateRegex(string pattern, string flags)
{
return new RegExp(pattern, flags);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Replace(this string input, RegExp pattern, string replacement)
{
return pattern.Replace(input, replacement);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsTruthy(string? s)
{
Expand Down
16 changes: 12 additions & 4 deletions src/platform/svg/SvgCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ export abstract class SvgCanvas implements ICanvas {
public settings!: Settings;

public beginRender(width: number, height: number): void {
this.buffer = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width | 0}px" height="${
height | 0
}px" class="at-surface-svg">\n`;
this.buffer = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width | 0}px" height="${height | 0
}px" class="at-surface-svg">\n`;
this._currentPath = '';
this._currentPathIsEmpty = true;
}
Expand Down Expand Up @@ -137,10 +136,19 @@ export abstract class SvgCanvas implements ICanvas {
if (this.textAlign !== TextAlign.Left) {
s += ` text-anchor="${this.getSvgTextAlignment(this.textAlign)}"`;
}
s += `>${text}</text>`;
s += `>${SvgCanvas.escapeText(text)}</text>`;
this.buffer += s;
}

private static escapeText(text: string) {
return text
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}

protected getSvgTextAlignment(textAlign: TextAlign): string {
switch (textAlign) {
case TextAlign.Left:
Expand Down
151 changes: 92 additions & 59 deletions src/rendering/ScoreBarRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { ScoreBeatContainerGlyph } from '@src/rendering/ScoreBeatContainerGlyph'
import { ScoreRenderer } from '@src/rendering/ScoreRenderer';
import { AccidentalHelper } from '@src/rendering/utils/AccidentalHelper';
import { BeamDirection } from '@src/rendering/utils/BeamDirection';
import { BeamingHelper } from '@src/rendering/utils/BeamingHelper';
import { BeamingHelper, BeamingHelperDrawInfo } from '@src/rendering/utils/BeamingHelper';
import { RenderingResources } from '@src/RenderingResources';
import { Settings } from '@src/Settings';
import { ModelUtils } from '@src/model/ModelUtils';
Expand Down Expand Up @@ -354,71 +354,104 @@ export class ScoreBarRenderer extends BarRendererBase {
private calculateBeamYWithDirection(h: BeamingHelper, x: number, direction: BeamDirection): number {
let stemSize: number = this.getStemSize(h);

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

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

const lastBeat = h.beats[h.beats.length - 1];
const firstBeat = h.beats[0];
const lastBeat = h.beats[h.beats.length - 1];

// we use the min/max notes to place the beam along their real position
// we only want a maximum of 10 offset for their gradient
let maxDistance: number = 10 * this.scale;
// if the min note is not first or last, we can align notes directly to the position
// of the min note
const beatOfLowestNote = h.beatOfLowestNote;
const beatOfHighestNote = h.beatOfHighestNote;
if (
direction === BeamDirection.Down &&
beatOfLowestNote !== firstBeat &&
beatOfLowestNote !== lastBeat
) {
return this.getScoreY(this.accidentalHelper.getMaxLine(beatOfLowestNote)) + stemSize;
}
if (
direction === BeamDirection.Up &&
beatOfHighestNote !== firstBeat &&
beatOfHighestNote !== lastBeat
) {
return this.getScoreY(this.accidentalHelper.getMinLine(beatOfHighestNote)) - stemSize;
}
// 1. put direct diagonal line.
drawingInfo.startX = h.getBeatLineX(firstBeat);
drawingInfo.startY =
direction === BeamDirection.Up
? this.getScoreY(this.accidentalHelper.getMinLine(firstBeat)) - stemSize
: this.getScoreY(this.accidentalHelper.getMaxLine(firstBeat)) + stemSize;

let startX: number = h.getBeatLineX(firstBeat);
let startY: number =
direction === BeamDirection.Up
? this.getScoreY(this.accidentalHelper.getMinLine(firstBeat)) - stemSize
: this.getScoreY(this.accidentalHelper.getMaxLine(firstBeat)) + stemSize;
drawingInfo.endX = h.getBeatLineX(lastBeat);
drawingInfo.endY =
direction === BeamDirection.Up
? this.getScoreY(this.accidentalHelper.getMinLine(lastBeat)) - stemSize
: this.getScoreY(this.accidentalHelper.getMaxLine(lastBeat)) + stemSize;

// 2. ensure max height
// we use the min/max notes to place the beam along their real position
// we only want a maximum of 10 offset for their gradient
let maxDistance: number = 10 * this.scale;
if (direction === BeamDirection.Down && drawingInfo.startY > drawingInfo.endY && drawingInfo.startY - drawingInfo.endY > maxDistance) {
drawingInfo.endY = drawingInfo.startY - maxDistance;
}
if (direction === BeamDirection.Down && drawingInfo.endY > drawingInfo.startY && drawingInfo.endY - drawingInfo.startY > maxDistance) {
drawingInfo.startY = drawingInfo.endY - maxDistance;
}
if (direction === BeamDirection.Up && drawingInfo.startY < drawingInfo.endY && drawingInfo.endY - drawingInfo.startY > maxDistance) {
drawingInfo.endY = drawingInfo.startY + maxDistance;
}
if (direction === BeamDirection.Up && drawingInfo.endY < drawingInfo.startY && drawingInfo.startY - drawingInfo.endY > maxDistance) {
drawingInfo.startY = drawingInfo.endY + maxDistance;
}

let endX: number = h.getBeatLineX(lastBeat);
let endY: number =
direction === BeamDirection.Up
? this.getScoreY(this.accidentalHelper.getMinLine(lastBeat)) - stemSize
: this.getScoreY(this.accidentalHelper.getMaxLine(lastBeat)) + stemSize;
// 3. let middle elements shift up/down
if (h.beats.length > 1) {
// check if highest note shifts bar up or down
if (direction === BeamDirection.Up) {
let yNeededForHighestNote = this.getScoreY(this.accidentalHelper.getMinLine(h.beatOfHighestNote)) - stemSize;
const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfHighestNote));

const diff = yGivenByCurrentValues - yNeededForHighestNote;
if (diff > 0) {
drawingInfo.startY -= diff;
drawingInfo.endY -= diff;
}
} else {
let yNeededForLowestNote = this.getScoreY(this.accidentalHelper.getMaxLine(h.beatOfLowestNote)) + stemSize;
const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfLowestNote));

const diff = yNeededForLowestNote - yGivenByCurrentValues;
if (diff > 0) {
drawingInfo.startY += diff;
drawingInfo.endY += diff;
}
}

// ensure the maxDistance
if (direction === BeamDirection.Down && startY > endY && startY - endY > maxDistance) {
endY = startY - maxDistance;
}
if (direction === BeamDirection.Down && endY > startY && endY - startY > maxDistance) {
startY = endY - maxDistance;
}
if (direction === BeamDirection.Up && startY < endY && endY - startY > maxDistance) {
endY = startY + maxDistance;
}
if (direction === BeamDirection.Up && endY < startY && startY - endY > maxDistance) {
startY = endY + maxDistance;
}
// get the y position of the given beat on this curve
if (startX === endX) {
return startY;
// check if rest shifts bar up or down
if (h.minRestLine !== null || h.maxRestLine !== null) {
const barCount: number = ModelUtils.getIndex(h.shortestDuration) - 2;
let scaleMod: number = h.isGrace ? NoteHeadGlyph.GraceScale : 1;
let barSpacing: number = barCount *
(BarRendererBase.BeamSpacing + BarRendererBase.BeamThickness) * this.scale * scaleMod;
barSpacing += BarRendererBase.BeamSpacing;

if (direction === BeamDirection.Up && h.minRestLine !== null) {
let yNeededForRest = this.getScoreY(h.minRestLine!) - barSpacing;
const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfMinRestLine!));

const diff = yGivenByCurrentValues - yNeededForRest;
if (diff > 0) {
drawingInfo.startY -= diff;
drawingInfo.endY -= diff;
}
} else if (direction === BeamDirection.Down && h.maxRestLine !== null) {
let yNeededForRest = this.getScoreY(h.maxRestLine!) + barSpacing;
const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfMaxRestLine!));

const diff = yNeededForRest - yGivenByCurrentValues;
if (diff > 0) {
drawingInfo.startY += diff;
drawingInfo.endY += diff;
}
}
}
}
}
// y(x) = ( (y2 - y1) / (x2 - x1) ) * (x - x1) + y1;
return ((endY - startY) / (endX - startX)) * (x - startX) + startY;

return h.drawingInfos.get(direction)!.calcY(x);
}


Expand Down
8 changes: 6 additions & 2 deletions src/rendering/glyphs/ScoreBeatGlyph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase {
0,
0,
4 *
(this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) *
this.scale
(this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) *
this.scale
)
);
this.addGlyph(ghost);
Expand Down Expand Up @@ -129,6 +129,10 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase {
this.restGlyph.beat = this.container.beat;
this.restGlyph.beamingHelper = this.beamingHelper;
this.addGlyph(this.restGlyph);
if (this.beamingHelper) {
this.beamingHelper.applyRest(this.container.beat, line);
}

//
// Note dots
//
Expand Down
35 changes: 19 additions & 16 deletions src/rendering/utils/AccidentalHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ export class AccidentalHelper {
const previousRenderer = this._barRenderer.previousRenderer as ScoreBarRenderer;
if (previousRenderer) {
const tieOriginLine = previousRenderer.accidentalHelper.getNoteLine(note.tieOrigin!);
if(tieOriginLine === line) {
if (tieOriginLine === line) {
skipAccidental = true;
}
}
Expand Down Expand Up @@ -328,26 +328,29 @@ export class AccidentalHelper {
}

if (!isHelperNote) {
let lines: BeatLines;
if (this._beatLines.has(relatedBeat.id)) {
lines = this._beatLines.get(relatedBeat.id)!;
}
else {
lines = new BeatLines();
this._beatLines.set(relatedBeat.id, lines);
}

if (lines.minLine === -1000 || line < lines.minLine) {
lines.minLine = line;
}
if (lines.minLine === -1000 || line > lines.maxLine) {
lines.maxLine = line;
}
this.registerLine(relatedBeat, line);
}

return accidentalToSet;
}

private registerLine(relatedBeat: Beat, line: number) {
let lines: BeatLines;
if (this._beatLines.has(relatedBeat.id)) {
lines = this._beatLines.get(relatedBeat.id)!;
}
else {
lines = new BeatLines();
this._beatLines.set(relatedBeat.id, lines);
}
if (lines.minLine === -1000 || line < lines.minLine) {
lines.minLine = line;
}
if (lines.minLine === -1000 || line > lines.maxLine) {
lines.maxLine = line;
}
}

public getMaxLine(b: Beat): number {
return this._beatLines.has(b.id)
? this._beatLines.get(b.id)!.maxLine
Expand Down
Loading

0 comments on commit 7ea8d9e

Please sign in to comment.