diff --git a/src.csharp/AlphaTab.Test/VisualTests/VisualTestHelper.cs b/src.csharp/AlphaTab.Test/VisualTests/VisualTestHelper.cs index 2e97f59a1..5ff31032d 100644 --- a/src.csharp/AlphaTab.Test/VisualTests/VisualTestHelper.cs +++ b/src.csharp/AlphaTab.Test/VisualTests/VisualTestHelper.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; +using AlphaTab.Core; using AlphaTab.Core.EcmaScript; using AlphaTab.Importer; using AlphaTab.Io; @@ -15,133 +17,84 @@ namespace AlphaTab.VisualTests { partial class VisualTestHelper { - public static async Task RunVisualTest(string inputFile, Settings? settings = null, - IList? tracks = null, string? message = null, double tolerancePercent = 1, bool triggerResize = false) + private static async Task RunVisualTestScoreWithResize(Score score, IList widths, + IList referenceImages, Settings? settings, IList? tracks, string? message, + double tolerancePercent) { - try - { - inputFile = $"test-data/visual-tests/{inputFile}"; - var inputFileData = - await TestPlatform.LoadFile(inputFile); - var referenceFileName = TestPlatform.ChangeExtension(inputFile, ".png"); - var score = ScoreLoader.LoadScoreFromBytes(inputFileData, settings); - - await RunVisualTestScore(score, referenceFileName, settings, - tracks, message, tolerancePercent, triggerResize); - } - catch (Exception e) - { - Assert.Fail($"Failed to run visual test {e}"); - } - } - - public static async Task RunVisualTestTex(string tex, string referenceFileName, - Settings? settings = null, - IList? tracks = null, string? message = null) - { - try - { - settings ??= new Settings(); - - var importer = new AlphaTexImporter(); - importer.Init(ByteBuffer.FromString(tex), settings); - var score = importer.ReadScore(); - - await RunVisualTestScore(score, referenceFileName, settings, - tracks, message); - } - catch (Exception e) - { - Assert.Fail($"Failed to run visual test {e}"); - } - } - - public static async Task RunVisualTestScore(Score score, string referenceFileName, - Settings? settings = null, - IList? tracks = null, string? message = null, double tolerancePercent = 1, bool triggerResize = false) - { - settings ??= new Settings(); - tracks ??= new AlphaTab.Collections.List {0}; - - settings.Core.Engine = "skia"; - settings.Core.EnableLazyLoading = false; - settings.Core.UseWorkers = false; - - settings.Display.Resources.CopyrightFont.Family = "Roboto"; - settings.Display.Resources.TitleFont.Family = "PT Serif"; - settings.Display.Resources.SubTitleFont.Family = "PT Serif"; - settings.Display.Resources.WordsFont.Family = "PT Serif"; - settings.Display.Resources.EffectFont.Family = "PT Serif"; - settings.Display.Resources.FretboardNumberFont.Family = "Roboto"; - settings.Display.Resources.TablatureFont.Family = "Roboto"; - settings.Display.Resources.GraceFont.Family = "Roboto"; - settings.Display.Resources.BarNumberFont.Family = "Roboto"; - settings.Display.Resources.FingeringFont.Family = "PT Serif"; - settings.Display.Resources.MarkerFont.Family = "PT Serif"; - - LoadFonts(); + tracks ??= new List { 0 }; + PrepareSettingsForTest(ref settings); - if (!referenceFileName.StartsWith("test-data/")) + var referenceFileData = new List(); + foreach (var referenceFileName in referenceImages) { - referenceFileName = $"test-data/visual-tests/{referenceFileName}"; + if (referenceFileName == null) + { + referenceFileData.Add(null); + } + else + { + referenceFileData.Add(await TestPlatform.LoadFile(Path.Combine("test-data", "visual-tests", referenceFileName))); + } } - var referenceFileData = - await TestPlatform.LoadFile(referenceFileName); - - var result = new AlphaTab.Collections.List(); - var totalWidth = 0.0; - var totalHeight = 0.0; - var isResizeRender = false; + var results = new AlphaTab.Collections.List>(); + var totalWidths = new AlphaTab.Collections.List(); + var totalHeights = new AlphaTab.Collections.List(); var task = new TaskCompletionSource(); var renderer = new ScoreRenderer(settings) { - Width = 1300 + Width = widths.Shift() }; renderer.PreRender.On(isResize => { - result = new AlphaTab.Collections.List(); - totalWidth = 0.0; - totalHeight = 0.0; + results.Add(new AlphaTab.Collections.List()); + totalWidths.Add(0); + totalHeights.Add(0); }); renderer.PartialRenderFinished.On(e => { if (e != null) { - result.Add(e); + results[^1].Add(e); } }); renderer.RenderFinished.On(e => { - totalWidth = e.TotalWidth; - totalHeight = e.TotalHeight; - result.Add(e); - if(!triggerResize || isResizeRender) + totalWidths[^1] = e.TotalWidth; + totalHeights[^1] = e.TotalHeight; + results[^1].Add(e); + if (widths.Count > 0) { - task.SetResult(null); + renderer.Width = widths.Shift(); + renderer.ResizeRender(); } - else if(triggerResize) + else { - isResizeRender = true; - renderer.ResizeRender(); + task.SetResult(null); } }); renderer.Error.On((e) => { task.SetException(e); }); renderer.RenderScore(score, tracks); - if (await Task.WhenAny(task.Task, Task.Delay(2000)) == task.Task) + if (await Task.WhenAny(task.Task, Task.Delay(2000 * referenceImages.Count)) == task.Task) { - CompareVisualResult( - totalWidth, - totalHeight, - result, - referenceFileName, - referenceFileData, - message, - tolerancePercent - ); + for (var i = 0; i < results.Count; i++) + { + if (referenceImages[i] != null) + { + CompareVisualResult( + totalWidths[i], + totalHeights[i], + results[i], + referenceImages[i]!, + referenceFileData[i]!, + message, + tolerancePercent + ); + } + } } else { @@ -149,6 +102,28 @@ public static async Task RunVisualTestScore(Score score, string referenceFileNam } } + private static void PrepareSettingsForTest(ref Settings? settings) + { + settings ??= new Settings(); + settings.Core.Engine = "skia"; + settings.Core.EnableLazyLoading = false; + settings.Core.UseWorkers = false; + + settings.Display.Resources.CopyrightFont.Family = "Roboto"; + settings.Display.Resources.TitleFont.Family = "PT Serif"; + settings.Display.Resources.SubTitleFont.Family = "PT Serif"; + settings.Display.Resources.WordsFont.Family = "PT Serif"; + settings.Display.Resources.EffectFont.Family = "PT Serif"; + settings.Display.Resources.FretboardNumberFont.Family = "Roboto"; + settings.Display.Resources.TablatureFont.Family = "Roboto"; + settings.Display.Resources.GraceFont.Family = "Roboto"; + settings.Display.Resources.BarNumberFont.Family = "Roboto"; + settings.Display.Resources.FingeringFont.Family = "PT Serif"; + settings.Display.Resources.MarkerFont.Family = "PT Serif"; + + LoadFonts(); + } + private static bool _fontsLoaded; private static void LoadFonts() { @@ -220,6 +195,10 @@ private static void CompareVisualResult(double totalWidth, double totalHeight, { try { + Assert.AreEqual(totalWidth, referenceBitmap.Width, + "Width of images does not match"); + Assert.AreEqual(totalHeight, referenceBitmap.Height, + "Height of images does not match"); var diffData = new Uint8Array(finalBitmap.Bytes.Length); var match = PixelMatch.Match( new Uint8Array(referenceBitmap.Bytes), @@ -267,6 +246,10 @@ private static void CompareVisualResult(double totalWidth, double totalHeight, Assert.Fail(msg); } } + catch (AssertFailedException) + { + throw; + } catch (Exception e) { Assert.Fail($"Error comparing images: {e}, ${message}"); diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Math.cs b/src.csharp/AlphaTab/Core/EcmaScript/Math.cs index c0961ee27..40734bc98 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/Math.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/Math.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace AlphaTab.Core.EcmaScript { @@ -17,6 +18,16 @@ public static double Abs(double v) return System.Math.Abs(v); } + public static double Max(params double[] items) + { + return items.Max(); + } + + public static double Min(params double[] items) + { + return items.Min(); + } + public static double Max(double a, double b) { return System.Math.Max(a, b); diff --git a/src.csharp/AlphaTab/Core/TypeHelper.cs b/src.csharp/AlphaTab/Core/TypeHelper.cs index 8df7bcb63..5b9c3278c 100644 --- a/src.csharp/AlphaTab/Core/TypeHelper.cs +++ b/src.csharp/AlphaTab/Core/TypeHelper.cs @@ -14,6 +14,21 @@ public static IList CreateList(params T[] values) return new List(values); } + public static void Add(this IList list, IList newItems) + { + if(list is List l) + { + l.AddRange(newItems); + } + else + { + foreach (var i in newItems) + { + list.Add(i); + } + } + } + public static IList Splice(this IList data, double start, double deleteCount) { var items = data.GetRange((int) start, (int) deleteCount); @@ -101,6 +116,13 @@ public static void Unshift(this IList data, T item) data.Insert(0, item); } + public static T Shift(this IList data) + { + var i = data[0]; + data.RemoveAt(0); + return i; + } + public static T Pop(this IList data) { if (data.Count > 0) diff --git a/src.kotlin/alphaTab/alphaTab/build.gradle.kts b/src.kotlin/alphaTab/alphaTab/build.gradle.kts index 83964143f..db2717386 100644 --- a/src.kotlin/alphaTab/alphaTab/build.gradle.kts +++ b/src.kotlin/alphaTab/alphaTab/build.gradle.kts @@ -79,9 +79,7 @@ kotlin { } } - val androidAndroidTestRelease by getting val androidTest by getting { - dependsOn(androidAndroidTestRelease) dependencies { implementation(kotlin("test-junit")) implementation("junit:junit:4.13.2") diff --git a/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidUiFacade.kt b/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidUiFacade.kt index ba7de9c87..6277c1086 100644 --- a/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidUiFacade.kt +++ b/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidUiFacade.kt @@ -359,11 +359,11 @@ internal class AndroidUiFacade : IUiFacade { error: (arg1: Error) -> Unit ): Boolean { when (data) { - data is Score -> { + (data is Score) -> { success(data as Score) return true } - data is ByteArray -> { + (data is ByteArray) -> { success( ScoreLoader.loadScoreFromBytes( Uint8Array((data as ByteArray).asUByteArray()), @@ -372,7 +372,7 @@ internal class AndroidUiFacade : IUiFacade { ) return true } - data is UByteArray -> { + (data is UByteArray) -> { success( ScoreLoader.loadScoreFromBytes( Uint8Array((data as UByteArray)), @@ -381,7 +381,7 @@ internal class AndroidUiFacade : IUiFacade { ) return true } - data is InputStream -> { + (data is InputStream) -> { val bos = ByteArrayOutputStream() (data as InputStream).copyTo(bos) success( @@ -402,15 +402,15 @@ internal class AndroidUiFacade : IUiFacade { val player = api.player ?: return false when (data) { - data is ByteArray -> { + (data is ByteArray) -> { player.loadSoundFont(Uint8Array((data as ByteArray).asUByteArray()), append) return true } - data is UByteArray -> { + (data is UByteArray) -> { player.loadSoundFont(Uint8Array((data as UByteArray)), append) return true } - data is InputStream -> { + (data is InputStream) -> { val bos = ByteArrayOutputStream() (data as InputStream).copyTo(bos) player.loadSoundFont(Uint8Array(bos.toByteArray().asUByteArray()), append) diff --git a/src.kotlin/alphaTab/alphaTab/src/androidTest/kotlin/alphaTab/visualTests/VisualTestHelperPartials.kt b/src.kotlin/alphaTab/alphaTab/src/androidTest/kotlin/alphaTab/visualTests/VisualTestHelperPartials.kt index 50fa6ed73..1e267ddd4 100644 --- a/src.kotlin/alphaTab/alphaTab/src/androidTest/kotlin/alphaTab/visualTests/VisualTestHelperPartials.kt +++ b/src.kotlin/alphaTab/alphaTab/src/androidTest/kotlin/alphaTab/visualTests/VisualTestHelperPartials.kt @@ -26,73 +26,9 @@ import kotlin.contracts.ExperimentalContracts @ExperimentalUnsignedTypes public class VisualTestHelperPartials { companion object { - public fun runVisualTest( - inputFile: String, - settings: Settings? = null, - tracks: DoubleList? = null, - message: String? = null, - tolerancePercent: Double = 1.0, - triggerResize: Boolean = false - ) { - try { - val fullInputFile = "test-data/visual-tests/$inputFile" - val inputFileData = TestPlatformPartials.loadFile(fullInputFile) - val referenceFileName = TestPlatform.changeExtension(fullInputFile, ".png") - val score = ScoreLoader.loadScoreFromBytes(inputFileData, settings) - - runVisualTestScore( - score, - referenceFileName, - settings, - tracks, - message, - tolerancePercent, - triggerResize - ) - } catch (e: Throwable) { - Assert.fail("Failed to run visual test $e ${e.stackTraceToString()}") - } - } - - public fun runVisualTestTex( - tex: String, - referenceFileName: String, - settings: Settings? = null, - tracks: DoubleList? = null, - message: String? = null, - tolerancePercent: Double = 1.0, - triggerResize: Boolean = false - ) { - try { - val actualSettings = settings ?: Settings() - val importer = AlphaTexImporter() - importer.init(ByteBuffer.fromString(tex), actualSettings) - val score = importer.readScore() - - runVisualTestScore( - score, - referenceFileName, - settings, - tracks, - message, - tolerancePercent, - triggerResize - ) - } catch (e: Throwable) { - Assert.fail("Failed to run visual test $e") - } - } - private var _initialized: Boolean = false - public fun runVisualTestScore( - score: Score, - referenceFileName: String, - settings: Settings? = null, - tracks: DoubleList? = null, - message: String? = null, - tolerancePercent: Double = 1.0, - triggerResize: Boolean = false - ) { + + private fun prepareSettingsForTest(settings:Settings?) : Settings { if (!_initialized) { SkiaCanvas.initialize(TestPlatformPartials.loadFile("test-data/../font/bravura/Bravura.ttf")) Environment.renderEngines.set("skia", RenderEngineFactory(true) { SkiaCanvas() }) @@ -101,8 +37,6 @@ public class VisualTestHelperPartials { } val actualSettings = settings ?: Settings() - val actualTracks = tracks ?: DoubleList() - actualSettings.core.engine = "skia" actualSettings.core.enableLazyLoading = false actualSettings.core.useWorkers = false @@ -119,21 +53,36 @@ public class VisualTestHelperPartials { actualSettings.display.resources.fingeringFont.family = "PT Serif" actualSettings.display.resources.markerFont.family = "PT Serif" + return actualSettings + } - var actualReferenceFileName = referenceFileName - if (!actualReferenceFileName.startsWith("test-data/")) { - actualReferenceFileName = "test-data/visual-tests/$actualReferenceFileName" - } + public fun runVisualTestScoreWithResize( + score: Score, + widths: DoubleList, + referenceImages: alphaTab.collections.List, + settings: Settings? = null, + tracks: DoubleList? = null, + message: String? = null, + tolerancePercent: Double = 1.0 + ) { + val actualSettings = prepareSettingsForTest(settings) + val actualTracks = tracks ?: DoubleList(0.0) - val referenceFileData = TestPlatformPartials.loadFile(actualReferenceFileName) + val referenceFileData = ArrayList() + for (referenceFileName in referenceImages) { + if(referenceFileName == null) { + referenceFileData.add(null) + } else { + referenceFileData.add(TestPlatformPartials.loadFile("test-data/visual-tests/$referenceFileName")) + } + } - val result = ArrayList() - var totalWidth = 0.0 - var totalHeight = 0.0 - var isResizeRender = false + val results = ArrayList>() + var totalWidths = DoubleList() + var totalHeights = DoubleList() val renderer = ScoreRenderer(actualSettings) - renderer.width = 1300.0 + renderer.width = widths.shift() val waitHandle = Semaphore(1) waitHandle.acquire() @@ -141,20 +90,22 @@ public class VisualTestHelperPartials { var error: Throwable? = null renderer.preRender.on { _ -> - result.clear() + results.add(ArrayList()) + totalWidths.push(0.0) + totalHeights.push(0.0) } renderer.partialRenderFinished.on { e -> - result.add(e) + results.last().add(e) } renderer.renderFinished.on { e -> - totalWidth = e.totalWidth - totalHeight = e.totalHeight - result.add(e) - if (!triggerResize || isResizeRender) { - waitHandle.release() - } else { - isResizeRender = true + totalWidths[totalWidths.length.toInt() - 1] = e.totalWidth + totalHeights[totalHeights.length.toInt() - 1] = e.totalHeight + results.last().add(e) + if (widths.length > 0) { + renderer.width = widths.shift() renderer.resizeRender() + } else { + waitHandle.release() } } renderer.error.on { e -> @@ -171,19 +122,23 @@ public class VisualTestHelperPartials { } } - if (waitHandle.tryAcquire(2000, TimeUnit.MILLISECONDS)) { + if (waitHandle.tryAcquire(2000 * referenceImages.length.toLong(), TimeUnit.MILLISECONDS)) { if (error != null) { Assert.fail("Rendering failed with error $error ${error?.stackTraceToString()}") } else { - compareVisualResult( - totalWidth, - totalHeight, - result, - referenceFileName, - referenceFileData, - message, - tolerancePercent - ) + for((i,r) in results.withIndex()) { + if(referenceImages[i] != null) { + compareVisualResult( + totalWidths[i], + totalHeights[i], + r, + referenceImages[i]!!, + referenceFileData[i]!!, + message, + tolerancePercent + ) + } + } } } else { job.cancel() diff --git a/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleList.kt b/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleList.kt index 0ccc29045..17fa5d031 100644 --- a/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleList.kt +++ b/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/DoubleList.kt @@ -115,6 +115,17 @@ public class DoubleList : IDoubleIterable { _items.sort(0, _size) } + public fun shift(): Double { + val d = _items[0] + if (_items.size > 1) { + _items = _items.copyOfRange(1, _items.size) + } else { + _items = DoubleArray(0) + } + _size-- + return d + } + public override fun iterator(): DoubleIterator { return Iterator(this) } diff --git a/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/List.kt b/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/List.kt index 1e02326fa..d3068d39d 100644 --- a/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/List.kt +++ b/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/List.kt @@ -39,6 +39,10 @@ public class List : Iterable { _data.add(item) } + public fun push(items: List) { + _data.addAll(items._data) + } + public operator fun get(index: Int): T { return _data[index] } @@ -59,6 +63,10 @@ public class List : Iterable { return _data.removeLast() } + public fun unshift(item:T) { + _data.add(0, item) + } + public fun sort(comparison: (a: T, b: T) -> Double) : List { _data.sortWith { a, b -> comparison(a, b).toInt() } return this diff --git a/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Math.kt b/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Math.kt index 07dc4d21a..019d63d4a 100644 --- a/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Math.kt +++ b/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/core/ecmaScript/Math.kt @@ -61,10 +61,18 @@ internal class Math { return kotlin.math.min(a, b) } + public fun min(a: Double, b: Double, c:Double): Double { + return kotlin.math.min(kotlin.math.min(a, b), c); + } + public fun max(a: Double, b: Double): Double { return kotlin.math.max(a, b) } + public fun max(a: Double, b: Double, c:Double): Double { + return kotlin.math.max(kotlin.math.max(a, b), c); + } + public fun random(): Double { return kotlin.random.Random.nextDouble() } diff --git a/src/rendering/BarRendererBase.ts b/src/rendering/BarRendererBase.ts index d77acac84..2b6956f3d 100644 --- a/src/rendering/BarRendererBase.ts +++ b/src/rendering/BarRendererBase.ts @@ -82,6 +82,8 @@ export class BarRendererBase { private _voiceContainers: Map = new Map(); private _postBeatGlyphs: LeftToRightLayoutingGlyphGroup = new LeftToRightLayoutingGlyphGroup(); + private _ties: Glyph[] = []; + public get nextRenderer(): BarRendererBase | null { if (!this.bar || !this.bar.nextBar) { return null; @@ -131,20 +133,28 @@ export class BarRendererBase { } } + public registerTies(ties: Glyph[]) { + this._ties.push(...ties); + } + public get middleYPosition(): number { return 0; } - public registerOverflowTop(topOverflow: number): void { + public registerOverflowTop(topOverflow: number): boolean { if (topOverflow > this.topOverflow) { this.topOverflow = topOverflow; + return true; } + return false; } - public registerOverflowBottom(bottomOverflow: number): void { + public registerOverflowBottom(bottomOverflow: number): boolean { if (bottomOverflow > this.bottomOverflow) { this.bottomOverflow = bottomOverflow; + return true; } + return false; } public scaleToWidth(width: number): void { @@ -223,8 +233,32 @@ export class BarRendererBase { public isFinalized: boolean = false; - public finalizeRenderer(): void { + public finalizeRenderer(): boolean { this.isFinalized = true; + + let didChangeOverflows = false; + // allow spacing to be used for tie overflows + const barTop = this.y - this.staff.topSpacing; + const barBottom = this.y + this.height + this.staff.bottomSpacing; + for (const tie of this._ties) { + tie.doLayout(); + if (tie.height > 0) { + const bottomOverflow = tie.y + tie.height - barBottom; + if (bottomOverflow > 0) { + if (this.registerOverflowBottom(bottomOverflow)) { + didChangeOverflows = true; + } + } + const topOverflow = tie.y - barTop; + if (topOverflow < 0) { + if (this.registerOverflowTop(topOverflow * -1)) { + didChangeOverflows = true; + } + } + } + } + + return didChangeOverflows; } /** @@ -245,6 +279,7 @@ export class BarRendererBase { return; } this.helpers.initialize(); + this._ties = []; this._preBeatGlyphs = new LeftToRightLayoutingGlyphGroup(); this._preBeatGlyphs.renderer = this; this._voiceContainers.clear(); @@ -345,7 +380,7 @@ export class BarRendererBase { canvas ); // canvas.color = Color.random(); - // canvas.fillRect(cx + this.x + this._preBeatGlyphs.x, cy + this.y, this._preBeatGlyphs.width, this.height); + // canvas.fillRect(cx + this.x, cy + this.y, this.width, this.height); } public buildBoundingsLookup(masterBarBounds: MasterBarBounds, cx: number, cy: number): void { diff --git a/src/rendering/EffectBarRenderer.ts b/src/rendering/EffectBarRenderer.ts index 10e028792..dc64b34cb 100644 --- a/src/rendering/EffectBarRenderer.ts +++ b/src/rendering/EffectBarRenderer.ts @@ -34,14 +34,17 @@ export class EffectBarRenderer extends BarRendererBase { super.updateSizes(); } - public override finalizeRenderer(): void { - super.finalizeRenderer(); - this.updateHeight(); + public override finalizeRenderer(): boolean { + let didChange = super.finalizeRenderer(); + if (this.updateHeight()) { + didChange = true; + } + return didChange; } - private updateHeight(): void { + private updateHeight(): boolean { if (!this.sizingInfo) { - return; + return false; } let y: number = 0; for (let slot of this.sizingInfo.slots) { @@ -52,7 +55,11 @@ export class EffectBarRenderer extends BarRendererBase { } y += slot.shared.height; } - this.height = y; + if (y !== this.height) { + this.height = y; + return true; + } + return false; } public override applyLayoutingInfo(): boolean { diff --git a/src/rendering/ScoreBeatContainerGlyph.ts b/src/rendering/ScoreBeatContainerGlyph.ts index eb7d3f3f1..d34c150c6 100644 --- a/src/rendering/ScoreBeatContainerGlyph.ts +++ b/src/rendering/ScoreBeatContainerGlyph.ts @@ -33,7 +33,7 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { while (destination.nextBeat && destination.nextBeat.isLegatoDestination) { destination = destination.nextBeat; } - this.ties.push(new ScoreLegatoGlyph(this.beat, destination, false)); + this.addTie(new ScoreLegatoGlyph(this.beat, destination, false)); } } else if (this.beat.isLegatoDestination) { // only create slur for last destination of "group" @@ -42,7 +42,7 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { while (origin.previousBeat && origin.previousBeat.isLegatoOrigin) { origin = origin.previousBeat; } - this.ties.push(new ScoreLegatoGlyph(origin, this.beat, true)); + this.addTie(new ScoreLegatoGlyph(origin, this.beat, true)); } } if (this._bend) { @@ -69,32 +69,32 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { ) { // tslint:disable-next-line: no-unnecessary-type-assertion let tie: ScoreTieGlyph = new ScoreTieGlyph(n, n.tieDestination!, false); - this.ties.push(tie); + this.addTie(tie); } if (n.isTieDestination && !n.tieOrigin!.hasBend && !n.beat.hasWhammyBar) { let tie: ScoreTieGlyph = new ScoreTieGlyph(n.tieOrigin!, n, true); - this.ties.push(tie); + this.addTie(tie); } // TODO: depending on the type we have other positioning // we should place glyphs in the preNotesGlyph or postNotesGlyph if needed if (n.slideInType !== SlideInType.None || n.slideOutType !== SlideOutType.None) { let l: ScoreSlideLineGlyph = new ScoreSlideLineGlyph(n.slideInType, n.slideOutType, n, this); - this.ties.push(l); + this.addTie(l); } if (n.isSlurOrigin && n.slurDestination && n.slurDestination.isVisible) { // tslint:disable-next-line: no-unnecessary-type-assertion let tie: ScoreSlurGlyph = new ScoreSlurGlyph(n, n.slurDestination!, false); - this.ties.push(tie); + this.addTie(tie); } if (n.isSlurDestination) { let tie: ScoreSlurGlyph = new ScoreSlurGlyph(n.slurOrigin!, n, true); - this.ties.push(tie); + this.addTie(tie); } // start effect slur on first beat if (!this._effectSlur && n.isEffectSlurOrigin && n.effectSlurDestination) { const effectSlur = new ScoreSlurGlyph(n, n.effectSlurDestination, false); this._effectSlur = effectSlur; - this.ties.push(effectSlur); + this.addTie(effectSlur); } // end effect slur on last beat if (!this._effectEndSlur && n.beat.isEffectSlurDestination && n.beat.effectSlurOrigin) { @@ -104,14 +104,14 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { let endNote: Note = direction === BeamDirection.Up ? n.beat.minNote! : n.beat.maxNote!; const effectEndSlur = new ScoreSlurGlyph(startNote, endNote, true); this._effectEndSlur = effectEndSlur; - this.ties.push(effectEndSlur); + this.addTie(effectEndSlur); } if (n.hasBend) { if (!this._bend) { const bend = new ScoreBendGlyph(n.beat); this._bend = bend; bend.renderer = this.renderer; - this.ties.push(bend); + this.addTie(bend); } // tslint:disable-next-line: no-unnecessary-type-assertion this._bend!.addBends(n); diff --git a/src/rendering/glyphs/BeatContainerGlyph.ts b/src/rendering/glyphs/BeatContainerGlyph.ts index c1df82293..93880e017 100644 --- a/src/rendering/glyphs/BeatContainerGlyph.ts +++ b/src/rendering/glyphs/BeatContainerGlyph.ts @@ -15,7 +15,7 @@ import { FlagGlyph } from '@src/rendering/glyphs/FlagGlyph'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; export class BeatContainerGlyph extends Glyph { - public static readonly GraceBeatPadding:number = 3; + public static readonly GraceBeatPadding: number = 3; public voiceContainer: VoiceContainerGlyph; public beat: Beat; public preNotes!: BeatGlyphBase; @@ -34,24 +34,32 @@ export class BeatContainerGlyph extends Glyph { this.voiceContainer = voiceContainer; } + public addTie(tie: Glyph) { + tie.renderer = this.renderer; + this.ties.push(tie); + } + public registerLayoutingInfo(layoutings: BarLayoutingInfo): void { let preBeatStretch: number = this.preNotes.computedWidth + this.onNotes.centerX; - if(this.beat.graceGroup && !this.beat.graceGroup.isComplete) { + if (this.beat.graceGroup && !this.beat.graceGroup.isComplete) { preBeatStretch += BeatContainerGlyph.GraceBeatPadding * this.renderer.scale; } let postBeatStretch: number = this.onNotes.computedWidth - this.onNotes.centerX; // make space for flag const helper = this.renderer.helpers.getBeamingHelperForBeat(this.beat); - if(helper && helper.hasFlag || this.beat.graceType !== GraceType.None) { - postBeatStretch += (FlagGlyph.FlagWidth * this.scale * (this.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1)); + if ((helper && helper.hasFlag) || this.beat.graceType !== GraceType.None) { + postBeatStretch += + FlagGlyph.FlagWidth * + this.scale * + (this.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1); } - for(const tie of this.ties) { + for (const tie of this.ties) { postBeatStretch += tie.width; } // Add some further spacing to grace notes - if(this.beat.graceType !== GraceType.None) { + if (this.beat.graceType !== GraceType.None) { postBeatStretch += BeatContainerGlyph.GraceBeatPadding * this.renderer.scale; } @@ -64,7 +72,7 @@ export class BeatContainerGlyph extends Glyph { public applyLayoutingInfo(info: BarLayoutingInfo): void { let offset: number = info.getBeatCenterX(this.beat) - this.onNotes.centerX; - if(this.beat.graceGroup && !this.beat.graceGroup.isComplete) { + if (this.beat.graceGroup && !this.beat.graceGroup.isComplete) { offset += BeatContainerGlyph.GraceBeatPadding * this.renderer.scale; } @@ -89,6 +97,7 @@ export class BeatContainerGlyph extends Glyph { while (i >= 0) { this.createTies(this.beat.notes[i--]); } + this.renderer.registerTies(this.ties); this.updateWidth(); } @@ -121,9 +130,6 @@ export class BeatContainerGlyph extends Glyph { } public scaleToWidth(beatWidth: number): void { - for (let tie of this.ties) { - tie.doLayout(); - } this.onNotes.updateBeamingHelper(); this.width = beatWidth; } diff --git a/src/rendering/glyphs/TabBeatContainerGlyph.ts b/src/rendering/glyphs/TabBeatContainerGlyph.ts index 55914dc21..d5760700a 100644 --- a/src/rendering/glyphs/TabBeatContainerGlyph.ts +++ b/src/rendering/glyphs/TabBeatContainerGlyph.ts @@ -35,15 +35,15 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { let renderer: TabBarRenderer = this.renderer as TabBarRenderer; if (n.isTieOrigin && renderer.showTiedNotes && n.tieDestination!.isVisible) { let tie: TabTieGlyph = new TabTieGlyph(n, n.tieDestination!, false); - this.ties.push(tie); + this.addTie(tie); } if (n.isTieDestination && renderer.showTiedNotes) { let tie: TabTieGlyph = new TabTieGlyph(n.tieOrigin!, n, true); - this.ties.push(tie); + this.addTie(tie); } if (n.isLeftHandTapped && !n.isHammerPullDestination) { let tapSlur: TabTieGlyph = new TabTieGlyph(n, n, false); - this.ties.push(tapSlur); + this.addTie(tapSlur); } // start effect slur on first beat if (n.isEffectSlurOrigin && n.effectSlurDestination) { @@ -57,7 +57,7 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { if (!expanded) { let effectSlur: TabSlurGlyph = new TabSlurGlyph(n, n.effectSlurDestination, false, false); this._effectSlurs.push(effectSlur); - this.ties.push(effectSlur); + this.addTie(effectSlur); } } // end effect slur on last beat @@ -72,19 +72,19 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { if (!expanded) { let effectSlur: TabSlurGlyph = new TabSlurGlyph(n.effectSlurOrigin, n, false, true); this._effectSlurs.push(effectSlur); - this.ties.push(effectSlur); + this.addTie(effectSlur); } } if (n.slideInType !== SlideInType.None || n.slideOutType !== SlideOutType.None) { let l: TabSlideLineGlyph = new TabSlideLineGlyph(n.slideInType, n.slideOutType, n, this); - this.ties.push(l); + this.addTie(l); } if (n.hasBend) { if (!this._bend) { const bend = new TabBendGlyph(); this._bend = bend; bend.renderer = this.renderer; - this.ties.push(bend); + this.addTie(bend); } this._bend.addBends(n); } diff --git a/src/rendering/glyphs/TieGlyph.ts b/src/rendering/glyphs/TieGlyph.ts index ecd9a90fb..a53d6ab01 100644 --- a/src/rendering/glyphs/TieGlyph.ts +++ b/src/rendering/glyphs/TieGlyph.ts @@ -3,6 +3,7 @@ import { ICanvas } from '@src/platform/ICanvas'; import { BarRendererBase } from '@src/rendering/BarRendererBase'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; +import { Bounds } from '../utils/Bounds'; export class TieGlyph extends Glyph { protected startBeat: Beat | null; @@ -21,16 +22,21 @@ export class TieGlyph extends Glyph { this.forEnd = forEnd; } + private _startX: number = 0; + private _startY: number = 0; + private _endX: number = 0; + private _endY: number = 0; + private _tieHeight: number = 0; + private _shouldDraw: boolean = false; + public override doLayout(): void { this.width = 0; - } - - public override paint(cx: number, cy: number, canvas: ICanvas): void { + // TODO fix nullability of start/end beat, if (!this.endBeat) { + this._shouldDraw = false; return; } - // TODO fix nullability of start/end beat, let startNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( this.renderer.staff.staveId, this.startBeat!.voice.bar @@ -42,11 +48,12 @@ export class TieGlyph extends Glyph { ); this.endNoteRenderer = endNoteRenderer; - let startX: number = 0; - let endX: number = 0; - let startY: number = 0; - let endY: number = 0; - let shouldDraw: boolean = false; + this._startX = 0; + this._endX = 0; + this._startY = 0; + this._endY = 0; + this.height = 0; + this._shouldDraw = false; // if we are on the tie start, we check if we // either can draw till the end note, or we just can draw till the bar end this.tieDirection = !startNoteRenderer @@ -55,52 +62,77 @@ export class TieGlyph extends Glyph { if (!this.forEnd && startNoteRenderer) { // line break or bar break if (startNoteRenderer !== endNoteRenderer) { - startX = cx + startNoteRenderer.x + this.getStartX(); - startY = cy + startNoteRenderer.y + this.getStartY() + this.yOffset; + this._startX = startNoteRenderer.x + this.getStartX(); + this._startY = startNoteRenderer.y + this.getStartY() + this.yOffset; // line break: to bar end if (!endNoteRenderer || startNoteRenderer.staff !== endNoteRenderer.staff) { - endX = cx + startNoteRenderer.x + startNoteRenderer.width; - endY = startY; + this._endX = startNoteRenderer.x + startNoteRenderer.width; + this._endY = this._startY; } else { - endX = cx + endNoteRenderer.x + this.getEndX(); - endY = cy + endNoteRenderer.y + this.getEndY() + this.yOffset; + this._endX = endNoteRenderer.x + this.getEndX(); + this._endY = endNoteRenderer.y + this.getEndY() + this.yOffset; } } else { - startX = cx + startNoteRenderer.x + this.getStartX(); - endX = cx + endNoteRenderer.x + this.getEndX(); - startY = cy + startNoteRenderer.y + this.getStartY() + this.yOffset; - endY = cy + endNoteRenderer.y + this.getEndY() + this.yOffset; + this._startX = startNoteRenderer.x + this.getStartX(); + this._endX = endNoteRenderer.x + this.getEndX(); + this._startY = startNoteRenderer.y + this.getStartY() + this.yOffset; + this._endY = endNoteRenderer.y + this.getEndY() + this.yOffset; } - shouldDraw = true; + this._shouldDraw = true; } else if (!startNoteRenderer || startNoteRenderer.staff !== endNoteRenderer!.staff) { - startX = cx + endNoteRenderer!.x; - endX = cx + endNoteRenderer!.x + this.getEndX(); - startY = cy + endNoteRenderer!.y + this.getEndY() + this.yOffset; - endY = startY; - shouldDraw = true; + this._startX = endNoteRenderer!.x; + this._endX = endNoteRenderer!.x + this.getEndX(); + this._startY = endNoteRenderer!.y + this.getEndY() + this.yOffset; + this._endY = this._startY; + this._shouldDraw = true; + } + + if (this._shouldDraw) { + this.y = Math.min(this._startY, this._endY); + if (this.shouldDrawBendSlur()) { + this._tieHeight = 0; // TODO: Bend slur height to be considered? + } else { + this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY); + this.height = TieGlyph.calculateActualTieHeight( + this.renderer.scale, + this._startX, + this._startY, + this._endX, + this._endY, + this.tieDirection === BeamDirection.Down, + this._tieHeight, + 4 + ).h; + } + + if (this.tieDirection === BeamDirection.Up) { + this.y -= this.height; + } } - if (shouldDraw) { - if(this.shouldDrawBendSlur()) { + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + if (this._shouldDraw) { + if (this.shouldDrawBendSlur()) { TieGlyph.drawBendSlur( canvas, - startX, - startY, - endX, - endY, + cx + this._startX, + cy + this._startY, + cx + this._endX, + cy + this._endY, this.tieDirection === BeamDirection.Down, this.scale ); - } - else { + } else { TieGlyph.paintTie( canvas, this.scale, - startX, - startY, - endX, - endY, + cx + this._startX, + cy + this._startY, + cx + this._endX, + cy + this._endY, this.tieDirection === BeamDirection.Down, - this.getTieHeight(startX, startY, endX, endY), + this._tieHeight, 4 ); } @@ -135,19 +167,77 @@ export class TieGlyph extends Glyph { return 0; } - public static paintTie( - canvas: ICanvas, + public static calculateActualTieHeight( scale: number, x1: number, y1: number, x2: number, y2: number, - down: boolean = false, - offset: number = 22, - size: number = 4 - ): void { + down: boolean, + offset: number, + size: number + ): Bounds { + const cp = TieGlyph.computeBezierControlPoints(scale, x1, y1, x2, y2, down, offset, size); + + x1 = cp[0]; + y1 = cp[1]; + const cpx = cp[2]; + const cpy = cp[3]; + x2 = cp[6]; + y2 = cp[7]; + + const tx = (x1 - cpx) / (x1 - 2 * cpx + x2); + const ex = TieGlyph.calculateExtrema(x1, y1, cpx, cpy, x2, y2, tx); + const xMin = ex.length > 0 ? Math.min(x1, x2, ex[0]) : Math.min(x1, x2); + const xMax = ex.length > 0 ? Math.max(x1, x2, ex[0]) : Math.max(x1, x2); + + const ty = (y1 - cpy) / (y1 - 2 * cpy + y2); + const ey = TieGlyph.calculateExtrema(x1, y1, cpx, cpy, x2, y2, ty); + const yMin = ey.length > 0 ? Math.min(y1, y2, ey[1]) : Math.min(y1, y2); + const yMax = ey.length > 0 ? Math.max(y1, y2, ey[1]) : Math.max(y1, y2); + + const b = new Bounds(); + b.x = xMin; + b.y = yMin; + b.w = xMax - xMin; + b.h = yMax - yMin; + return b; + } + + private static calculateExtrema( + x1: number, + y1: number, + cpx: number, + cpy: number, + x2: number, + y2: number, + t: number + ): number[] { + if (t <= 0 || 1 <= t) { + return []; + } + + const c1x = x1 + (cpx - x1) * t; + const c1y = y1 + (cpy - y1) * t; + + const c2x = cpx + (x2 - cpx) * t; + const c2y = cpy + (y2 - cpy) * t; + + return [c1x + (c2x - c1x) * t, c1y + (c2y - c1y) * t]; + } + + private static computeBezierControlPoints( + scale: number, + x1: number, + y1: number, + x2: number, + y2: number, + down: boolean, + offset: number, + size: number + ): number[] { if (x1 === x2 && y1 === y2) { - return; + return []; } // ensure endX > startX @@ -185,12 +275,44 @@ export class TieGlyph extends Glyph { let cp1Y: number = centerY + offset * normalVectorY; let cp2X: number = centerX + (offset - size) * normalVectorX; let cp2Y: number = centerY + (offset - size) * normalVectorY; + + return [x1, y1, cp1X, cp1Y, cp2X, cp2Y, x2, y2]; + } + + public static paintTie( + canvas: ICanvas, + scale: number, + x1: number, + y1: number, + x2: number, + y2: number, + down: boolean = false, + offset: number = 22, + size: number = 4 + ): void { + const cps = TieGlyph.computeBezierControlPoints(scale, x1, y1, x2, y2, down, offset, size); + canvas.beginPath(); - canvas.moveTo(x1, y1); - canvas.quadraticCurveTo(cp1X, cp1Y, x2, y2); - canvas.quadraticCurveTo(cp2X, cp2Y, x1, y1); + canvas.moveTo(cps[0], cps[1]); + canvas.quadraticCurveTo(cps[2], cps[3], cps[6], cps[7]); + canvas.quadraticCurveTo(cps[4], cps[5], cps[0], cps[1]); canvas.closePath(); canvas.fill(); + + // const c = canvas.color; + // canvas.color = Color.random(100); + // canvas.fillCircle(cps[0], cps[1], 4); + // canvas.fillCircle(cps[2], cps[3], 4); + // canvas.fillCircle(cps[4], cps[5], 4); + // canvas.fillCircle(cps[7], cps[6], 4); + + // canvas.color = Color.random(100); + + // const bbox = TieGlyph.calculateActualTieHeight(scale, x1, y1, x2, y2, down, offset, size); + + // canvas.fillRect(bbox.x, bbox.y, bbox.w, bbox.h); + + // canvas.color = c; } private static readonly BendSlurHeight: number = 11; diff --git a/src/rendering/layout/HorizontalScreenLayout.ts b/src/rendering/layout/HorizontalScreenLayout.ts index 57311ed7c..b8fec4992 100644 --- a/src/rendering/layout/HorizontalScreenLayout.ts +++ b/src/rendering/layout/HorizontalScreenLayout.ts @@ -139,7 +139,7 @@ export class HorizontalScreenLayout extends ScoreLayout { null ); } - this._group.finalizeGroup(); + this.finalizeGroup(); this.height = Math.floor(this._group.y + this._group.height); this.width = this._group.x + this._group.width + this._pagePadding[2]; currentBarIndex = 0; @@ -194,4 +194,9 @@ export class HorizontalScreenLayout extends ScoreLayout { this.height = this.layoutAndRenderAnnotation(this.height) + this._pagePadding[3]; } + + private finalizeGroup() { + this._group!.scaleToWidth(this._group!.width); + this._group!.finalizeGroup(); + } } diff --git a/src/rendering/layout/PageViewLayout.ts b/src/rendering/layout/PageViewLayout.ts index 1af47e076..9aa1b98c6 100644 --- a/src/rendering/layout/PageViewLayout.ts +++ b/src/rendering/layout/PageViewLayout.ts @@ -231,7 +231,6 @@ export class PageViewLayout extends ScoreLayout { for (let i: number = 0; i < this._groups.length; i++) { let group: StaveGroup = this._groups[i]; this.fitGroup(group); - group.finalizeGroup(); y += this.paintGroup(group, oldHeight); } } else { @@ -262,7 +261,6 @@ export class PageViewLayout extends ScoreLayout { group.isLast = this.lastBarIndex === group.lastBarIndex; this._groups.push(group); this.fitGroup(group); - group.finalizeGroup(); y += this.paintGroup(group, oldHeight); // note: we do not increase currentIndex here to have it added to the next group group = this.createEmptyStaveGroup(); @@ -274,7 +272,6 @@ export class PageViewLayout extends ScoreLayout { group.isLast = this.lastBarIndex === group.lastBarIndex; // don't forget to finish the last group this.fitGroup(group); - group.finalizeGroup(); y += this.paintGroup(group, oldHeight); } return y; @@ -294,7 +291,6 @@ export class PageViewLayout extends ScoreLayout { currentBarIndex = group.lastBarIndex + 1; // finalize group (sizing etc). this.fitGroup(group); - group.finalizeGroup(); Logger.debug( this.name, 'Rendering partial from bar ' + group.firstBarIndex + ' to ' + group.lastBarIndex, @@ -341,6 +337,10 @@ export class PageViewLayout extends ScoreLayout { if (group.isFull || group.width > this.maxWidth) { group.scaleToWidth(this.maxWidth); } + else { + group.scaleToWidth(group.width); + } + group.finalizeGroup(); } private createStaveGroup(currentBarIndex: number, endIndex: number): StaveGroup { diff --git a/src/rendering/staves/RenderStaff.ts b/src/rendering/staves/RenderStaff.ts index 7adc95f06..4442f29d9 100644 --- a/src/rendering/staves/RenderStaff.ts +++ b/src/rendering/staves/RenderStaff.ts @@ -129,8 +129,15 @@ export class RenderStaff { // the space over the bar renderers, for now we evenly apply the space to all bars let difference: number = width - this.staveGroup.width; let spacePerBar: number = difference / this.barRenderers.length; + let x = 0; + let topOverflow: number = this.topOverflow; for (let i: number = 0, j: number = this.barRenderers.length; i < j; i++) { - this.barRenderers[i].scaleToWidth(this.barRenderers[i].width + spacePerBar); + this.barRenderers[i].x = x; + this.barRenderers[i].y = this.topSpacing + topOverflow; + if(difference !== 0) { + this.barRenderers[i].scaleToWidth(this.barRenderers[i].width + spacePerBar); + } + x += this.barRenderers[i].width; } } @@ -157,19 +164,32 @@ export class RenderStaff { } public finalizeStaff(): void { - let x: number = 0; this.height = 0; + + // 1st pass: let all renderers finalize themselves, this might cause + // changes in the overflows + let needsSecondPass = false; let topOverflow: number = this.topOverflow; - let bottomOverflow: number = this.bottomOverflow; for (let i: number = 0; i < this.barRenderers.length; i++) { - this.barRenderers[i].x = x; this.barRenderers[i].y = this.topSpacing + topOverflow; this.height = Math.max(this.height, this.barRenderers[i].height); - this.barRenderers[i].finalizeRenderer(); - x += this.barRenderers[i].width; + if (this.barRenderers[i].finalizeRenderer()) { + needsSecondPass = true; + } + } + + // 2nd pass: move renderers to correct position respecting the new overflows + if (needsSecondPass) { + topOverflow = this.topOverflow; + for (let i: number = 0; i < this.barRenderers.length; i++) { + this.barRenderers[i].y = this.topSpacing + topOverflow; + this.height = Math.max(this.height, this.barRenderers[i].height); + this.barRenderers[i].finalizeRenderer(); + } } + if (this.height > 0) { - this.height += this.topSpacing + topOverflow + bottomOverflow + this.bottomSpacing; + this.height += this.topSpacing + topOverflow + this.bottomOverflow + this.bottomSpacing; } } diff --git a/test-data/visual-tests/music-notation/beams-advanced.png b/test-data/visual-tests/music-notation/beams-advanced.png index c3e26ff04..8b843ca27 100644 Binary files a/test-data/visual-tests/music-notation/beams-advanced.png and b/test-data/visual-tests/music-notation/beams-advanced.png differ diff --git a/test-data/visual-tests/notation-legend/full-default.png b/test-data/visual-tests/notation-legend/full-default.png index 929b4fa15..c6b9025bb 100644 Binary files a/test-data/visual-tests/notation-legend/full-default.png and b/test-data/visual-tests/notation-legend/full-default.png differ diff --git a/test-data/visual-tests/notation-legend/full-songbook.png b/test-data/visual-tests/notation-legend/full-songbook.png index e2952572b..b3998f0a4 100644 Binary files a/test-data/visual-tests/notation-legend/full-songbook.png and b/test-data/visual-tests/notation-legend/full-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/mixed-default.png b/test-data/visual-tests/notation-legend/mixed-default.png index 60f6229be..fbdcc5331 100644 Binary files a/test-data/visual-tests/notation-legend/mixed-default.png and b/test-data/visual-tests/notation-legend/mixed-default.png differ diff --git a/test-data/visual-tests/notation-legend/mixed-songbook.png b/test-data/visual-tests/notation-legend/mixed-songbook.png index 1fc4732ed..d2bad783f 100644 Binary files a/test-data/visual-tests/notation-legend/mixed-songbook.png and b/test-data/visual-tests/notation-legend/mixed-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/resize-sequence-1300.png b/test-data/visual-tests/notation-legend/resize-sequence-1300.png new file mode 100644 index 000000000..9f80e156f Binary files /dev/null and b/test-data/visual-tests/notation-legend/resize-sequence-1300.png differ diff --git a/test-data/visual-tests/notation-legend/resize-sequence-1500.png b/test-data/visual-tests/notation-legend/resize-sequence-1500.png new file mode 100644 index 000000000..6962a0df9 Binary files /dev/null and b/test-data/visual-tests/notation-legend/resize-sequence-1500.png differ diff --git a/test-data/visual-tests/notation-legend/resize-sequence-500.png b/test-data/visual-tests/notation-legend/resize-sequence-500.png new file mode 100644 index 000000000..47ac135ca Binary files /dev/null and b/test-data/visual-tests/notation-legend/resize-sequence-500.png differ diff --git a/test-data/visual-tests/notation-legend/resize-sequence-800.png b/test-data/visual-tests/notation-legend/resize-sequence-800.png new file mode 100644 index 000000000..0b427fe47 Binary files /dev/null and b/test-data/visual-tests/notation-legend/resize-sequence-800.png differ diff --git a/test-data/visual-tests/notation-legend/tap-riff-default.png b/test-data/visual-tests/notation-legend/tap-riff-default.png index 7a5749481..b30923ef6 100644 Binary files a/test-data/visual-tests/notation-legend/tap-riff-default.png and b/test-data/visual-tests/notation-legend/tap-riff-default.png differ diff --git a/test-data/visual-tests/notation-legend/tap-riff-songbook.png b/test-data/visual-tests/notation-legend/tap-riff-songbook.png index 7a5749481..b30923ef6 100644 Binary files a/test-data/visual-tests/notation-legend/tap-riff-songbook.png and b/test-data/visual-tests/notation-legend/tap-riff-songbook.png differ diff --git a/test/audio/MidiTickLookup.test.ts b/test/audio/MidiTickLookup.test.ts index 2613e8ecb..a6645b5a4 100644 --- a/test/audio/MidiTickLookup.test.ts +++ b/test/audio/MidiTickLookup.test.ts @@ -5,7 +5,7 @@ import { Settings } from '@src/Settings'; import { TestPlatform } from '@test/TestPlatform'; describe('MidiTickLookupTest', () => { - async function buildLookup(score:Score, settings:Settings): Promise { + function buildLookup(score:Score, settings:Settings): MidiTickLookup { const midiFile = new MidiFile(); const handler = new AlphaSynthMidiFileHandler(midiFile); const midiFileGenerator = new MidiFileGenerator(score, settings, handler); @@ -17,7 +17,7 @@ describe('MidiTickLookupTest', () => { const buffer = await TestPlatform.loadFile('test-data/audio/cursor-snapping.gp'); const settings = new Settings(); const score = ScoreLoader.loadScoreFromBytes(buffer, settings); - const lookup = await buildLookup(score, settings); + const lookup = buildLookup(score, settings); // initial lookup should detect correctly first rest on first voice // with the quarter rest on the second voice as next beat diff --git a/test/visualTests/PixelMatch.ts b/test/visualTests/PixelMatch.ts index 5c2084f20..0d4a95b5f 100644 --- a/test/visualTests/PixelMatch.ts +++ b/test/visualTests/PixelMatch.ts @@ -92,7 +92,7 @@ export class PixelMatch { options: PixelMatchOptions ): PixelMatchResult { if (img1.length !== img2.length || (output && output.length !== img1.length)) { - throw new Error('Image sizes do not match.'); + throw new Error(`Image sizes do not match. ${img1.length} !== ${img2.length}`); } if (img1.length !== width * height * 4) throw new Error('Image data size does not match width/height.'); diff --git a/test/visualTests/VisualTestHelper.ts b/test/visualTests/VisualTestHelper.ts index 6e812bcd2..b4698b02e 100644 --- a/test/visualTests/VisualTestHelper.ts +++ b/test/visualTests/VisualTestHelper.ts @@ -15,10 +15,6 @@ import { JsonConverter } from '@src/model/JsonConverter'; * @partial */ export class VisualTestHelper { - /** - * @target web - * @partial - */ public static async runVisualTest( inputFile: string, settings?: Settings, @@ -46,10 +42,33 @@ export class VisualTestHelper { } } - /** - * @target web - * @partial - */ + public static async runVisualTestWithResize( + inputFile: string, + widths: number[], + referenceImages: string[], + settings?: Settings, + tracks?: number[], + message?: string, + tolerancePercent: number = 1 + ): Promise { + try { + const inputFileData = await TestPlatform.loadFile(`test-data/visual-tests/${inputFile}`); + let score: Score = ScoreLoader.loadScoreFromBytes(inputFileData, settings); + + await VisualTestHelper.runVisualTestScoreWithResize( + score, + widths, + referenceImages, + settings, + tracks, + message, + tolerancePercent + ); + } catch (e) { + fail(`Failed to run visual test ${e}`); + } + } + public static async runVisualTestTex( tex: string, referenceFileName: string, @@ -67,7 +86,7 @@ export class VisualTestHelper { importer.init(ByteBuffer.fromString(tex), settings); let score: Score = importer.readScore(); - await VisualTestHelper.runVisualTestScore(score, referenceFileName, settings, tracks, message); + await VisualTestHelper.runVisualTestScore(score, referenceFileName, settings, tracks, message, tolerancePercent); } catch (e) { fail(`Failed to run visual test ${e}`); } @@ -147,10 +166,6 @@ export class VisualTestHelper { } } - /** - * @target web - * @partial - */ public static async runVisualTestScore( score: Score, referenceFileName: string, @@ -159,6 +174,40 @@ export class VisualTestHelper { message?: string, tolerancePercent: number = 1, triggerResize: boolean = false + ): Promise { + const widths = [1300]; + if (triggerResize) { + widths.push(widths[0]); + } + + const referenceImages: (string | null)[] = [referenceFileName]; + if (triggerResize) { + referenceImages.unshift(null); + } + + await VisualTestHelper.runVisualTestScoreWithResize( + score, + widths, + referenceImages, + settings, + tracks, + message, + tolerancePercent + ); + } + + /** + * @target web + * @partial + */ + public static async runVisualTestScoreWithResize( + score: Score, + widths: number[], + referenceImages: (string | null)[], + settings?: Settings, + tracks?: number[], + message?: string, + tolerancePercent: number = 1 ): Promise { try { if (!settings) { @@ -168,87 +217,53 @@ export class VisualTestHelper { tracks = [0]; } - settings.core.fontDirectory = CoreSettings.ensureFullUrl('/base/font/bravura/'); - settings.core.engine = 'html5'; - Environment.HighDpiFactor = 1; // test data is in scale 1 - settings.core.enableLazyLoading = false; - - settings.display.resources.copyrightFont.families = ['Roboto']; - settings.display.resources.titleFont.families = ['PT Serif']; - settings.display.resources.subTitleFont.families = ['PT Serif']; - settings.display.resources.wordsFont.families = ['PT Serif']; - settings.display.resources.effectFont.families = ['PT Serif']; - settings.display.resources.fretboardNumberFont.families = ['Roboto']; - settings.display.resources.tablatureFont.families = ['Roboto']; - settings.display.resources.graceFont.families = ['Roboto']; - settings.display.resources.barNumberFont.families = ['Roboto']; - settings.display.resources.fingeringFont.families = ['PT Serif']; - settings.display.resources.markerFont.families = ['PT Serif']; - - await VisualTestHelper.loadFonts(); - - let referenceFileData: Uint8Array; - try { - referenceFileData = await TestPlatform.loadFile(`test-data/visual-tests/${referenceFileName}`); - } catch (e) { - referenceFileData = new Uint8Array(0); + await VisualTestHelper.prepareSettingsForTest(settings); + + let referenceFileData: (Uint8Array | null)[] = []; + for (const img of referenceImages) { + try { + if (img !== null) { + referenceFileData.push(await TestPlatform.loadFile(`test-data/visual-tests/${img}`)); + } else { + referenceFileData.push(null); + } + } catch (e) { + referenceFileData.push(new Uint8Array(0)); + } } const renderElement = document.createElement('div'); - renderElement.style.width = '1300px'; + renderElement.style.width = `${widths.shift()}px`; renderElement.style.position = 'absolute'; renderElement.style.visibility = 'hidden'; document.body.appendChild(renderElement); - // here we need to trick a little bit, normally SVG does not require the font to be loaded - // before rendering starts, but in our case we need it to convert it later for diffing to raster. - // so we initiate the bravura load and wait for it before proceeding with rendering. - Environment.createStyleElement(document, settings.core.fontDirectory); - await Promise.race([ - new Promise((resolve, reject) => { - if (Environment.bravuraFontChecker.isFontLoaded) { - resolve(); - } else { - Environment.bravuraFontChecker.fontLoaded.on(() => { - resolve(); - }); - Environment.bravuraFontChecker.checkForFontAvailability(); - } - }), - new Promise((_, reject) => { - setTimeout(() => { - reject(new Error('Font loading did not complete in time')); - }, 2000); - }) - ]); - - let result: RenderFinishedEventArgs[] = []; - let totalWidth: number = 0; - let totalHeight: number = 0; - let isResizeRender = false; + let results: RenderFinishedEventArgs[][] = []; + let totalWidths: number[] = []; + let totalHeights: number[] = []; let render = new Promise((resolve, reject) => { const api = new AlphaTabApi(renderElement, settings); - api.renderStarted.on(isResize => { - result = []; - totalWidth = 0; - totalHeight = 0; + api.renderStarted.on(_ => { + results.push([]); + totalWidths.push(0); + totalHeights.push(0); }); api.renderer.partialRenderFinished.on(e => { if (e) { - result.push(e); + results[results.length - 1].push(e); } }); api.renderer.renderFinished.on(e => { - totalWidth = e.totalWidth; - totalHeight = e.totalHeight; - result.push(e); + totalWidths[totalWidths.length - 1] = e.totalWidth; + totalHeights[totalHeights.length - 1] = e.totalHeight; + results[results.length - 1].push(e); - if (!triggerResize || isResizeRender) { - resolve(); - } else if (triggerResize) { - isResizeRender = true; - // @ts-ignore + if (widths.length > 0) { + renderElement.style.width = `${widths.shift()}px`; + // @ts-ignore api.triggerResize(); + } else { + resolve(); } }); api.error.on(e => { @@ -266,24 +281,86 @@ export class VisualTestHelper { new Promise((_, reject) => { setTimeout(() => { reject(new Error('Rendering did not complete in time')); - }, 2000); + }, 2000 * widths.length); }) ]); - await VisualTestHelper.compareVisualResult( - totalWidth, - totalHeight, - result, - referenceFileName, - referenceFileData, - message, - tolerancePercent - ); + for (let i = 0; i < results.length; i++) { + if (referenceImages[i] !== null) { + await VisualTestHelper.compareVisualResult( + totalWidths[i], + totalHeights[i], + results[i], + referenceImages[i]!, + referenceFileData[i]!, + message, + tolerancePercent + ); + } + } } catch (e) { fail(`Failed to run visual test ${e}`); } } + /** + * @target web + * @partial + */ + static async waitForFonts(settings: Settings) { + // here we need to trick a little bit, normally SVG does not require the font to be loaded + // before rendering starts, but in our case we need it to convert it later for diffing to raster. + // so we initiate the bravura load and wait for it before proceeding with rendering. + Environment.createStyleElement(document, settings.core.fontDirectory); + await Promise.race([ + new Promise((resolve, reject) => { + if (Environment.bravuraFontChecker.isFontLoaded) { + resolve(); + } else { + Environment.bravuraFontChecker.fontLoaded.on(() => { + resolve(); + }); + Environment.bravuraFontChecker.checkForFontAvailability(); + } + }), + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Font loading did not complete in time')); + }, 2000); + }) + ]); + } + + /** + * @target web + * @partial + */ + static async prepareSettingsForTest(settings: Settings) { + settings.core.fontDirectory = CoreSettings.ensureFullUrl('/base/font/bravura/'); + settings.core.engine = 'html5'; + Environment.HighDpiFactor = 1; // test data is in scale 1 + settings.core.enableLazyLoading = false; + + settings.display.resources.copyrightFont.families = ['Roboto']; + settings.display.resources.titleFont.families = ['PT Serif']; + settings.display.resources.subTitleFont.families = ['PT Serif']; + settings.display.resources.wordsFont.families = ['PT Serif']; + settings.display.resources.effectFont.families = ['PT Serif']; + settings.display.resources.fretboardNumberFont.families = ['Roboto']; + settings.display.resources.tablatureFont.families = ['Roboto']; + settings.display.resources.graceFont.families = ['Roboto']; + settings.display.resources.barNumberFont.families = ['Roboto']; + settings.display.resources.fingeringFont.families = ['PT Serif']; + settings.display.resources.markerFont.families = ['PT Serif']; + + await VisualTestHelper.loadFonts(); + + // here we need to trick a little bit, normally SVG does not require the font to be loaded + // before rendering starts, but in our case we need it to convert it later for diffing to raster. + // so we initiate the bravura load and wait for it before proceeding with rendering. + await VisualTestHelper.waitForFonts(settings); + } + /** * @target web * @partial @@ -391,9 +468,7 @@ export class VisualTestHelper { * @target web * @partial */ - private static toEqualVisually( - _utils: jasmine.MatchersUtil, - ): jasmine.CustomAsyncMatcher { + private static toEqualVisually(_utils: jasmine.MatchersUtil): jasmine.CustomAsyncMatcher { return { async compare( actual: HTMLCanvasElement, diff --git a/test/visualTests/features/NotationLegend.test.ts b/test/visualTests/features/NotationLegend.test.ts index 4bce469d2..b275aeac6 100644 --- a/test/visualTests/features/NotationLegend.test.ts +++ b/test/visualTests/features/NotationLegend.test.ts @@ -99,6 +99,21 @@ describe('NotationLegend', () => { it('tied-note-accidentals-default', async () => { await runNotationLegendTest(`tied-note-accidentals-default.png`, 1, -1, false, 'tied-note-accidentals.gp'); }); it('tied-note-accidentals-songbook', async () => { await runNotationLegendTest(`tied-note-accidentals-songbook.png`, 1, -1, true, 'tied-note-accidentals.gp'); }); + it('resize-sequence', async () => { + let settings: Settings = new Settings(); + await VisualTestHelper.runVisualTestWithResize( + 'notation-legend/notation-legend.gp', + [1300, 800, 1500, 500], + [ + 'notation-legend/resize-sequence-1300.png', + 'notation-legend/resize-sequence-800.png', + 'notation-legend/resize-sequence-1500.png', + 'notation-legend/resize-sequence-500.png' + ], + settings, + [0]); + }); + async function runNotationLegendTest(referenceFileName: string, startBar: number, barCount: number, songBook: boolean, fileName: string = 'notation-legend.gp', tolerance:number=1): Promise { let settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Horizontal;