diff --git a/packages/alphatab/.env b/packages/alphatab/.env index 4bca2fb57..33ccbd705 100644 --- a/packages/alphatab/.env +++ b/packages/alphatab/.env @@ -1,3 +1,2 @@ FORCE_COLOR=1 -NODE_OPTIONS=--expose-gc -UPDATE_SNAPSHOT=true \ No newline at end of file +NODE_OPTIONS=--expose-gc \ No newline at end of file diff --git a/packages/alphatab/src/AlphaTabApiBase.ts b/packages/alphatab/src/AlphaTabApiBase.ts index f9a42895f..4f6b9f95c 100644 --- a/packages/alphatab/src/AlphaTabApiBase.ts +++ b/packages/alphatab/src/AlphaTabApiBase.ts @@ -10,7 +10,7 @@ import { import { AlphaTexImporter } from '@coderline/alphatab/importer/AlphaTexImporter'; import { Logger } from '@coderline/alphatab/Logger'; import { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler'; -import type { BeatTickLookupItem } from '@coderline/alphatab/midi/BeatTickLookup'; +import type { BeatTickLookupItem, IBeatVisibilityChecker } from '@coderline/alphatab/midi/BeatTickLookup'; import type { MetaDataEvent, MetaEvent, @@ -122,6 +122,22 @@ export interface PlaybackHighlightChangeEventArgs { highlightBlocks?: Bounds[]; } +/** + * @internal + */ +class BoundsLookupVisibilityChecker implements IBeatVisibilityChecker { + public bounds: BoundsLookup | null = null; + + public isVisible(beat: Beat): boolean { + const bounds = this.bounds; + if (!bounds) { + return false; + } + + return bounds.findBeat(beat) !== null; + } +} + /** * This class represents the public API of alphaTab and provides all logic to display * a music sheet in any UI using the given {@link IUiFacade} @@ -132,6 +148,7 @@ export class AlphaTabApiBase { private _startTime: number = 0; private _trackIndexes: number[] | null = null; private _trackIndexLookup: Set | null = null; + private readonly _beatVisibilityChecker = new BoundsLookupVisibilityChecker(); private _isDestroyed: boolean = false; private _score: Score | null = null; private _tracks: Track[] = []; @@ -2049,18 +2066,19 @@ export class AlphaTabApiBase { const cache: MidiTickLookup | null = this._tickCache; if (cache) { - const tracks = this._trackIndexLookup; - if (tracks != null && tracks.size > 0) { - const beat: MidiTickLookupFindBeatResult | null = cache.findBeat(tracks, tick, this._currentBeat); - if (beat) { - this._cursorUpdateBeat( - beat, - stop, - shouldScroll, - cursorSpeed, - forceUpdate || this.playerState === PlayerState.Paused - ); - } + const beat: MidiTickLookupFindBeatResult | null = cache.findBeatWithChecker( + this._beatVisibilityChecker, + tick, + this._currentBeat + ); + if (beat) { + this._cursorUpdateBeat( + beat, + stop, + shouldScroll, + cursorSpeed, + forceUpdate || this.playerState === PlayerState.Paused + ); } } } @@ -3420,6 +3438,8 @@ export class AlphaTabApiBase { return; } + this._beatVisibilityChecker.bounds = this.boundsLookup; + this._currentBeat = null; this._cursorUpdateTick(this._previousTick, false, 1, true, true); diff --git a/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts b/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts index 4ac08c97c..dc5fe4d92 100644 --- a/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts +++ b/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts @@ -59,6 +59,9 @@ export class RenderStylesheetSerializer { } } o.set("extendbarlines", obj.extendBarLines); + o.set("hideemptystaves", obj.hideEmptyStaves); + o.set("hideemptystavesinfirstsystem", obj.hideEmptyStavesInFirstSystem); + o.set("showsinglestaffbrackets", obj.showSingleStaffBrackets); return o; } public static setProperty(obj: RenderStylesheet, property: string, v: unknown): boolean { @@ -120,6 +123,15 @@ export class RenderStylesheetSerializer { case "extendbarlines": obj.extendBarLines = v! as boolean; return true; + case "hideemptystaves": + obj.hideEmptyStaves = v! as boolean; + return true; + case "hideemptystavesinfirstsystem": + obj.hideEmptyStavesInFirstSystem = v! as boolean; + return true; + case "showsinglestaffbrackets": + obj.showSingleStaffBrackets = v! as boolean; + return true; } return false; } diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts index 1461dd28e..a10dd1dde 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts @@ -190,7 +190,10 @@ export class AlphaTex1LanguageDefinitions { ['firstsystemtracknameorientation', [[[[10, 17], 0, ['horizontal', 'vertical']]]]], ['othersystemstracknameorientation', [[[[10, 17], 0, ['horizontal', 'vertical']]]]], ['extendbarlines', null], - ['chorddiagramsinscore', [[[[10], 1, ['true', 'false']]]]] + ['chorddiagramsinscore', [[[[10], 1, ['true', 'false']]]]], + ['hideemptystaves', null], + ['hideemptystavesinfirstsystem', null], + ['showsinglestaffbrackets', null] ]); public static readonly staffMetaDataSignatures = AlphaTex1LanguageDefinitions._signatures([ ['tuning', [[[[10, 17], 0, ['piano', 'none', 'voice']]], [[[10, 17], 5]]]], @@ -530,6 +533,9 @@ export class AlphaTex1LanguageDefinitions { ['othersystemstracknameorientation', null], ['extendbarlines', null], ['chorddiagramsinscore', null], + ['hideemptystaves', null], + ['hideemptystavesinfirstsystem', null], + ['showsinglestaffbrackets', null], [ 'tuning', [ diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index fab749e45..19f4eaf51 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -264,6 +264,15 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler ? AlphaTex1LanguageHandler._booleanLikeValue(metaData.arguments!.arguments, 0) : true; return ApplyNodeResult.Applied; + case 'hideemptystaves': + score.stylesheet.hideEmptyStaves = true; + return ApplyNodeResult.Applied; + case 'hideemptystavesinfirstsystem': + score.stylesheet.hideEmptyStavesInFirstSystem = true; + return ApplyNodeResult.Applied; + case 'showsinglestaffbrackets': + score.stylesheet.showSingleStaffBrackets = true; + return ApplyNodeResult.Applied; default: return ApplyNodeResult.NotAppliedUnrecognizedMarker; @@ -2497,6 +2506,18 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler if (stylesheet.globalDisplayChordDiagramsInScore) { nodes.push(Atnf.meta('chordDiagramsInScore')); } + + if (stylesheet.hideEmptyStaves) { + nodes.push(Atnf.meta('hideEmptyStaves')); + } + + if (stylesheet.hideEmptyStavesInFirstSystem) { + nodes.push(Atnf.meta('hideEmptyStavesInFirstSystem')); + } + + if (stylesheet.showSingleStaffBrackets) { + nodes.push(Atnf.meta('showSingleStaffBrackets')); + } // Unsupported: // 'globaldisplaychorddiagramsontop', diff --git a/packages/alphatab/src/midi/BeatTickLookup.ts b/packages/alphatab/src/midi/BeatTickLookup.ts index 8b7b2f0f5..a33983bb6 100644 --- a/packages/alphatab/src/midi/BeatTickLookup.ts +++ b/packages/alphatab/src/midi/BeatTickLookup.ts @@ -21,6 +21,15 @@ export class BeatTickLookupItem { } } +/** + * Classes implementing this interface can help in checking whether beats are currently being + * displayed so that they can be considered for a tick-search. + * @public + */ +export interface IBeatVisibilityChecker { + isVisible(beat: Beat): boolean; +} + /** * Represents the time period, for which one or multiple {@link Beat}s are played * @public @@ -96,4 +105,18 @@ export class BeatTickLookup { } return null; } + + /** + * Looks for the first visible beat which starts at this lookup so it can be used for cursor placement. + * @param checker The custom checker to see if a beat is visible. + * @returns The first beat which is visible according to the given tracks or null. + */ + getVisibleBeatAtStartWithChecker(checker: IBeatVisibilityChecker): Beat | null { + for (const b of this.highlightedBeats) { + if (b.playbackStart === this.start && checker.isVisible(b.beat)) { + return b.beat; + } + } + return null; + } } diff --git a/packages/alphatab/src/midi/MidiTickLookup.ts b/packages/alphatab/src/midi/MidiTickLookup.ts index 3e9617de2..c1881d4b9 100644 --- a/packages/alphatab/src/midi/MidiTickLookup.ts +++ b/packages/alphatab/src/midi/MidiTickLookup.ts @@ -1,5 +1,5 @@ import { Logger } from '@coderline/alphatab/Logger'; -import type { BeatTickLookup } from '@coderline/alphatab/midi/BeatTickLookup'; +import type { BeatTickLookup, IBeatVisibilityChecker } from '@coderline/alphatab/midi/BeatTickLookup'; import { MasterBarTickLookup } from '@coderline/alphatab/midi/MasterBarTickLookup'; import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; import type { Beat } from '@coderline/alphatab/model/Beat'; @@ -124,6 +124,19 @@ export class MidiTickLookupFindBeatResult { } } +/** + * @internal + */ +class TrackLookupBeatVisibilityChecker implements IBeatVisibilityChecker { + private _lookup: Set; + public constructor(lookup: Set) { + this._lookup = lookup; + } + public isVisible(beat: Beat): boolean { + return this._lookup.has(beat.voice.bar.staff.track.index); + } +} + /** * This class holds all information about when {@link MasterBar}s and {@link Beat}s are played. * @@ -191,21 +204,35 @@ export class MidiTickLookup { trackLookup: Set, tick: number, currentBeatHint: MidiTickLookupFindBeatResult | null = null + ): MidiTickLookupFindBeatResult | null { + return this.findBeatWithChecker(new TrackLookupBeatVisibilityChecker(trackLookup), tick, currentBeatHint); + } + /** + * Finds the currently played beat given a list of tracks and the current time. + * @param checker The checker to ask whether a beat is visible and should be considered for result. + * @param tick The current time in midi ticks. + * @param currentBeatHint Used for optimized lookup during playback. By passing in a previous result lookup of the next one can be optimized using heuristics. (optional). + * @returns The information about the current beat or null if no beat could be found. + */ + public findBeatWithChecker( + checker: IBeatVisibilityChecker, + tick: number, + currentBeatHint: MidiTickLookupFindBeatResult | null = null ): MidiTickLookupFindBeatResult | null { let result: MidiTickLookupFindBeatResult | null = null; if (currentBeatHint) { - result = this._findBeatFast(trackLookup, currentBeatHint, tick); + result = this._findBeatFast(checker, currentBeatHint, tick); } if (!result) { - result = this._findBeatSlow(trackLookup, currentBeatHint, tick, false); + result = this._findBeatSlow(checker, currentBeatHint, tick, false); } return result; } private _findBeatFast( - trackLookup: Set, + checker: IBeatVisibilityChecker, currentBeatHint: MidiTickLookupFindBeatResult, tick: number ): MidiTickLookupFindBeatResult | null { @@ -214,10 +241,15 @@ export class MidiTickLookup { return currentBeatHint; } // already on the next beat? - if (currentBeatHint.nextBeat && tick >= currentBeatHint.nextBeat.start && tick < currentBeatHint.nextBeat.end) { + if ( + currentBeatHint.nextBeat && + tick >= currentBeatHint.nextBeat.start && + tick < currentBeatHint.nextBeat.end && + (checker === undefined || checker.isVisible(currentBeatHint.nextBeat.beat)) + ) { const next = currentBeatHint.nextBeat!; // fill next in chain - this._fillNextBeat(next, trackLookup); + this._fillNextBeat(next, checker); return next; } @@ -225,7 +257,10 @@ export class MidiTickLookup { return null; } - private _fillNextBeatMultiBarRest(current: MidiTickLookupFindBeatResult, trackLookup: Set) { + private _fillNextBeatMultiBarRest( + current: MidiTickLookupFindBeatResult, + checker: IBeatVisibilityChecker + ) { const group = this.multiBarRestInfo!.get(current.masterBar.masterBar.index)!; // this is a bit sensitive. we assume that the sequence of multi-rest bars and the @@ -242,7 +277,7 @@ export class MidiTickLookup { // one more following -> use start of next if (endMasterBar.nextMasterBar) { current.nextBeat = this._firstBeatInMasterBar( - trackLookup, + checker, endMasterBar.nextMasterBar!, endMasterBar.nextMasterBar!.start, true @@ -284,25 +319,31 @@ export class MidiTickLookup { current.calculateDuration(); } - private _fillNextBeat(current: MidiTickLookupFindBeatResult, trackLookup: Set) { + private _fillNextBeat( + current: MidiTickLookupFindBeatResult, + checker: IBeatVisibilityChecker + ) { // on multibar rests take the duration until the end. if (this._isMultiBarRestResult(current)) { - this._fillNextBeatMultiBarRest(current, trackLookup); + this._fillNextBeatMultiBarRest(current, checker); } else { - this._fillNextBeatDefault(current, trackLookup); + this._fillNextBeatDefault(current, checker); } } - private _fillNextBeatDefault(current: MidiTickLookupFindBeatResult, trackLookup: Set) { + private _fillNextBeatDefault( + current: MidiTickLookupFindBeatResult, + checker: IBeatVisibilityChecker + ) { current.nextBeat = this._findBeatInMasterBar( current.masterBar, current.beatLookup.nextBeat, current.end, - trackLookup, + checker, true ); if (current.nextBeat == null) { - current.nextBeat = this._findBeatSlow(trackLookup, current, current.end, true); + current.nextBeat = this._findBeatSlow(checker, current, current.end, true); } // if we have the next beat take the difference between the times as duration @@ -344,7 +385,7 @@ export class MidiTickLookup { } private _findBeatSlow( - trackLookup: Set, + checker: IBeatVisibilityChecker, currentBeatHint: MidiTickLookupFindBeatResult | null, tick: number, isNextSearch: boolean @@ -376,11 +417,11 @@ export class MidiTickLookup { return null; } - return this._firstBeatInMasterBar(trackLookup, masterBar, tick, isNextSearch); + return this._firstBeatInMasterBar(checker, masterBar, tick, isNextSearch); } private _firstBeatInMasterBar( - trackLookup: Set, + checker: IBeatVisibilityChecker, startMasterBar: MasterBarTickLookup, tick: number, isNextSearch: boolean @@ -389,7 +430,13 @@ export class MidiTickLookup { // scan through beats and find first one which has a beat visible while (masterBar) { if (masterBar.firstBeat) { - const beat = this._findBeatInMasterBar(masterBar, masterBar.firstBeat, tick, trackLookup, isNextSearch); + const beat = this._findBeatInMasterBar( + masterBar, + masterBar.firstBeat, + tick, + checker, + isNextSearch + ); if (beat) { return beat; @@ -414,7 +461,7 @@ export class MidiTickLookup { masterBar: MasterBarTickLookup, currentStartLookup: BeatTickLookup | null, tick: number, - visibleTracks: Set, + checker: IBeatVisibilityChecker, isNextSearch: boolean ): MidiTickLookupFindBeatResult | null { if (!currentStartLookup) { @@ -434,7 +481,7 @@ export class MidiTickLookup { relativeTick < currentStartLookup.end ) { startBeatLookup = currentStartLookup; - startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks); + startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker); // found the matching beat lookup but none of the beats are visible // in this case scan further to the next lookup which has any visible beat @@ -443,7 +490,7 @@ export class MidiTickLookup { let currentMasterBar: MasterBarTickLookup | null = masterBar; while (currentMasterBar != null && startBeat == null) { while (currentStartLookup != null) { - startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks); + startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker); if (startBeat) { startBeatLookup = currentStartLookup; @@ -463,7 +510,7 @@ export class MidiTickLookup { let currentMasterBar: MasterBarTickLookup | null = masterBar; while (currentMasterBar != null && startBeat == null) { while (currentStartLookup != null) { - startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks); + startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker); if (startBeat) { startBeatLookup = currentStartLookup; @@ -492,7 +539,7 @@ export class MidiTickLookup { return null; } - const result = this._createResult(masterBar, startBeatLookup!, startBeat, isNextSearch, visibleTracks); + const result = this._createResult(masterBar, startBeatLookup!, startBeat, isNextSearch, checker); return result; } @@ -502,7 +549,7 @@ export class MidiTickLookup { beatLookup: BeatTickLookup, beat: Beat, isNextSearch: boolean, - visibleTracks: Set + checker: IBeatVisibilityChecker ) { const result = new MidiTickLookupFindBeatResult(masterBar); @@ -513,7 +560,7 @@ export class MidiTickLookup { if (!isNextSearch) { // the next beat filling will adjust this result with the respective durations - this._fillNextBeat(result, visibleTracks); + this._fillNextBeat(result, checker); } // if we do not search for the next beat, we need to still stretch multi-bar-rest // otherwise the fast path will not work correctly diff --git a/packages/alphatab/src/model/RenderStylesheet.ts b/packages/alphatab/src/model/RenderStylesheet.ts index 83825cbe2..689d596c6 100644 --- a/packages/alphatab/src/model/RenderStylesheet.ts +++ b/packages/alphatab/src/model/RenderStylesheet.ts @@ -164,4 +164,24 @@ export class RenderStylesheet { * Whether barlines should be drawn across staves within the same system. */ public extendBarLines: boolean = false; + + /** + * Whether to hide empty staves. + */ + public hideEmptyStaves: boolean = false; + + /** + * Whether to also hide empty staves in the first system. + * @remarks + * Only has an effect when activating {@link hideEmptyStaves}. + */ + public hideEmptyStavesInFirstSystem :boolean = false; + + /** + * Whether to show brackets and braces across single staves. + * @remarks + * This allows a more consistent view for identifying staves when using + * {@link hideEmptyStaves} + */ + public showSingleStaffBrackets: boolean = false; } diff --git a/packages/alphatab/src/rendering/glyphs/ClefGlyph.ts b/packages/alphatab/src/rendering/glyphs/ClefGlyph.ts index 920b52622..d0d853ea0 100644 --- a/packages/alphatab/src/rendering/glyphs/ClefGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ClefGlyph.ts @@ -1,10 +1,11 @@ +import { BarSubElement } from '@coderline/alphatab/model/Bar'; import { Clef } from '@coderline/alphatab/model/Clef'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; -import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { BarSubElement } from '@coderline/alphatab/model/Bar'; /** * @internal @@ -12,6 +13,7 @@ import { BarSubElement } from '@coderline/alphatab/model/Bar'; export class ClefGlyph extends MusicFontGlyph { private _clef: Clef; private _clefOttava: Ottavia; + private _ottavaGlyph?: MusicFontGlyph; public constructor(x: number, y: number, clef: Clef, clefOttava: Ottavia) { super(x, y, 1, ClefGlyph._getSymbol(clef, clefOttava)); @@ -19,11 +21,80 @@ export class ClefGlyph extends MusicFontGlyph { this._clefOttava = clefOttava; } + public override getBoundingBoxTop(): number { + let top = super.getBoundingBoxTop(); + + const ottava = this._ottavaGlyph; + if (ottava) { + const ottavaTop = this.y + ottava.getBoundingBoxTop(); + top = ModelUtils.minBoundingBox(top, ottavaTop); + } + + return top; + } + + public override getBoundingBoxBottom(): number { + let bottom = super.getBoundingBoxBottom(); + + const ottava = this._ottavaGlyph; + if (ottava) { + const ottavaBottom = this.y + ottava.getBoundingBoxBottom(); + bottom = ModelUtils.maxBoundingBox(bottom, ottavaBottom); + } + + + return bottom; + } + public override doLayout(): void { this.center = true; super.doLayout(); this.width = this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.GClef)!; this.offsetX = this.width / 2; + + this._ottavaGlyph = undefined; + switch (this._clef) { + case Clef.C3: + case Clef.C4: + switch (this._clefOttava) { + case Ottavia._8vb: + return; + } + break; + case Clef.F4: + case Clef.G2: + return; + } + + let ottavaSymbol: MusicFontSymbol; + let top: boolean = false; + switch (this._clefOttava) { + case Ottavia._15ma: + ottavaSymbol = MusicFontSymbol.Clef15; + top = true; + break; + case Ottavia._8va: + ottavaSymbol = MusicFontSymbol.Clef8; + top = true; + break; + case Ottavia._8vb: + ottavaSymbol = MusicFontSymbol.Clef8; + break; + case Ottavia._15mb: + ottavaSymbol = MusicFontSymbol.Clef15; + break; + default: + return; + } + const ottavaX = this.width / 2; + const ottavaY = top + ? this.renderer.smuflMetrics.glyphTop.get(this.symbol)! + : this.renderer.smuflMetrics.glyphBottom.get(this.symbol)! - + this.renderer.smuflMetrics.glyphHeights.get(ottavaSymbol)!; + this._ottavaGlyph = new MusicFontGlyph(ottavaX, -ottavaY, 1, ottavaSymbol); + this._ottavaGlyph!.center = true; + this._ottavaGlyph!.renderer = this.renderer; + this._ottavaGlyph!.doLayout(); } private static _getSymbol(clef: Clef, clefOttava: Ottavia): MusicFontSymbol { @@ -74,46 +145,9 @@ export class ClefGlyph extends MusicFontGlyph { super.paint(cx, cy, canvas); - switch (this._clef) { - case Clef.C3: - case Clef.C4: - switch (this._clefOttava) { - case Ottavia._8vb: - return; - } - break; - case Clef.F4: - case Clef.G2: - return; + const ottava = this._ottavaGlyph; + if (ottava) { + ottava.paint(cx + this.x, cy + this.y, canvas); } - - let ottavaGlyph: MusicFontSymbol; - let top: boolean = false; - switch (this._clefOttava) { - case Ottavia._15ma: - ottavaGlyph = MusicFontSymbol.Clef15; - top = true; - break; - case Ottavia._8va: - ottavaGlyph = MusicFontSymbol.Clef8; - top = true; - break; - case Ottavia._8vb: - ottavaGlyph = MusicFontSymbol.Clef8; - break; - case Ottavia._15mb: - ottavaGlyph = MusicFontSymbol.Clef15; - break; - default: - return; - } - const ottavaX: number = this.width / 2; - const ottavaY = top - ? this.renderer.smuflMetrics.glyphTop.get(this.symbol)! - : this.renderer.smuflMetrics.glyphBottom.get(this.symbol)! - - this.renderer.smuflMetrics.glyphHeights.get(ottavaGlyph)!; - CanvasHelper.fillMusicFontSymbolSafe(canvas,cx + this.x + ottavaX, cy + this.y - ottavaY, 1, ottavaGlyph, true) - - } } diff --git a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts index b167c6ac2..aacf95941 100644 --- a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts +++ b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts @@ -67,7 +67,7 @@ export class HorizontalScreenLayout extends ScoreLayout { endBarIndex = startIndex + endBarIndex - 1; // map count to array index endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex)); - this._system = this.createEmptyStaffSystem(); + this._system = this.createEmptyStaffSystem(0); this._system.isLast = true; this._system.x = this.pagePadding![0]; this._system.y = this.pagePadding![1]; diff --git a/packages/alphatab/src/rendering/layout/PageViewLayout.ts b/packages/alphatab/src/rendering/layout/PageViewLayout.ts index 769650c52..b73a53e28 100644 --- a/packages/alphatab/src/rendering/layout/PageViewLayout.ts +++ b/packages/alphatab/src/rendering/layout/PageViewLayout.ts @@ -227,8 +227,7 @@ export class PageViewLayout extends ScoreLayout { this._systems = []; let currentIndex: number = 0; const maxWidth: number = this._maxWidth; - let system: StaffSystem = this.createEmptyStaffSystem(); - system.index = this._systems.length; + let system: StaffSystem = this.createEmptyStaffSystem(this._systems.length); system.x = this.pagePadding![0]; system.y = y; while (currentIndex < this._allMasterBarRenderers.length) { @@ -262,8 +261,7 @@ export class PageViewLayout extends ScoreLayout { this._fitSystem(system); y += this._paintSystem(system, oldHeight); // note: we do not increase currentIndex here to have it added to the next system - system = this.createEmptyStaffSystem(); - system.index = this._systems.length; + system = this.createEmptyStaffSystem(this._systems.length); system.x = this.pagePadding![0]; system.y = y; } @@ -362,8 +360,7 @@ export class PageViewLayout extends ScoreLayout { } private _createStaffSystem(currentBarIndex: number, endIndex: number): StaffSystem { - const system: StaffSystem = this.createEmptyStaffSystem(); - system.index = this._systems.length; + const system: StaffSystem = this.createEmptyStaffSystem(this._systems.length); const barsPerRow: number = this._getBarsPerSystem(system.index); const maxWidth: number = this._maxWidth; const end: number = endIndex + 1; diff --git a/packages/alphatab/src/rendering/layout/ScoreLayout.ts b/packages/alphatab/src/rendering/layout/ScoreLayout.ts index d33a5be29..8bc3c5cad 100644 --- a/packages/alphatab/src/rendering/layout/ScoreLayout.ts +++ b/packages/alphatab/src/rendering/layout/ScoreLayout.ts @@ -374,8 +374,9 @@ export abstract class ScoreLayout { public lastBarIndex: number = 0; - protected createEmptyStaffSystem(): StaffSystem { + protected createEmptyStaffSystem(index:number): StaffSystem { const system: StaffSystem = new StaffSystem(this); + system.index = index; const allFactories = Environment.defaultRenderers; const renderStaves: RenderStaff[] = []; @@ -392,7 +393,7 @@ export abstract class ScoreLayout { for (const factory of allFactories) { if (this.profile.has(factory.staffId) && factory.canCreate(track, staff)) { - const renderStaff = new RenderStaff(trackIndex, staff, factory); + const renderStaff = new RenderStaff(system, trackIndex, staff, factory); // insert shared effect bands at front renderStaff.topEffectInfos.splice(0, 0, ...sharedTopEffects); renderStaff.bottomEffectInfos.push(...sharedBottomEffects); diff --git a/packages/alphatab/src/rendering/staves/RenderStaff.ts b/packages/alphatab/src/rendering/staves/RenderStaff.ts index 706f6c79d..726567d17 100644 --- a/packages/alphatab/src/rendering/staves/RenderStaff.ts +++ b/packages/alphatab/src/rendering/staves/RenderStaff.ts @@ -30,8 +30,11 @@ export class RenderStaff { public index: number = 0; public staffIndex: number = 0; + public isVisible = false; + private _emptyBarCount = 0; + public get isFirstInSystem() { - return this.index === 0; + return this.system.firstVisibleStaff === this; } public topEffectInfos: EffectBandInfo[] = []; @@ -75,10 +78,11 @@ export class RenderStaff { return this.y + this.topPadding + this.topOverflow + this.staffBottom; } - public constructor(trackIndex: number, staff: Staff, factory: BarRendererFactory) { + public constructor(system: StaffSystem, trackIndex: number, staff: Staff, factory: BarRendererFactory) { this._factory = factory; this.trackIndex = trackIndex; this.modelStaff = staff; + this.system = system; for (const b of factory.effectBands) { if (b.shouldCreate && !b.shouldCreate!(staff)) { continue; @@ -96,6 +100,8 @@ export class RenderStaff { break; } } + + this._updateVisibility(); } public getSharedLayoutData(key: string, def: T): T { @@ -127,6 +133,21 @@ export class RenderStaff { renderer.reLayout(); this.barRenderers.push(renderer); this.system.layout.registerBarRenderer(this.staffId, renderer); + if (renderer.bar.isEmpty || renderer.bar.isRestOnly) { + this._emptyBarCount++; + } + this._updateVisibility(); + } + + private _updateVisibility() { + const stylesheet = this.modelStaff.track.score.stylesheet; + const canHideEmptyStaves = + stylesheet.hideEmptyStaves && (stylesheet.hideEmptyStavesInFirstSystem || this.system.index > 0); + if (canHideEmptyStaves) { + this.isVisible = this._emptyBarCount < this.barRenderers.length; + } else { + this.isVisible = true; + } } public addBar(bar: Bar, layoutingInfo: BarLayoutingInfo, additionalMultiBarsRestBars: Bar[] | null): void { @@ -153,6 +174,10 @@ export class RenderStaff { this.barRenderers.push(renderer); this.system.layout.registerBarRenderer(this.staffId, renderer); + if (bar.isEmpty || bar.isRestOnly) { + this._emptyBarCount++; + } + this._updateVisibility(); } public revertLastBar(): BarRendererBase { @@ -164,7 +189,13 @@ export class RenderStaff { this.bottomOverflow = 0; for (const r of this.barRenderers) { r.afterStaffBarReverted(); - } + } + + if (lastBar.bar.isEmpty || lastBar.bar.isRestOnly) { + this._emptyBarCount--; + } + this._updateVisibility(); + return lastBar; } @@ -296,6 +327,8 @@ export class RenderStaff { } this.height = Math.ceil(this.height); + + this._updateVisibility(); } public paint(cx: number, cy: number, canvas: ICanvas, startIndex: number, count: number): void { diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index 0d9a70686..47ed0e3a8 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -28,21 +28,56 @@ import { StaffSystemBounds } from '@coderline/alphatab/rendering/utils/StaffSyst * @internal */ export abstract class SystemBracket { - public firstStaffInBracket: RenderStaff | null = null; - public lastStaffInBracket: RenderStaff | null = null; + private _system: StaffSystem; + public firstStaffInBracket?: RenderStaff; + public lastStaffInBracket?: RenderStaff; + public firstVisibleStaffInBracket?: RenderStaff; + public lastVisibleStaffInBracket?: RenderStaff; public drawAsBrace: boolean = false; public braceScale: number = 1; public width: number = 0; public index: number = 0; - public get canPaint(): boolean { - return this.firstStaffInBracket !== null && this.lastStaffInBracket !== null; + + public canPaint = false; + + public constructor(system: StaffSystem) { + this._system = system; } public abstract includesStaff(s: RenderStaff): boolean; + public updateCanPaint() { + let firstVisibleStaff: RenderStaff | undefined = undefined; + let lastVisibleStaff: RenderStaff | undefined = undefined; + for (let i = this.firstStaffInBracket!.index; i <= this.lastStaffInBracket!.index; i++) { + const staff = this._system.allStaves[i]; + if (staff.isVisible) { + if (!firstVisibleStaff) { + firstVisibleStaff = staff; + } + lastVisibleStaff = staff; + } + } + this.firstVisibleStaffInBracket = firstVisibleStaff; + this.lastVisibleStaffInBracket = lastVisibleStaff; + + if (!firstVisibleStaff || !lastVisibleStaff) { + this.canPaint = false; + return; + } + + // single staff brackets? + const singleStaffBrackets = this._system.layout.renderer.score!.stylesheet.showSingleStaffBrackets; + if (!singleStaffBrackets && firstVisibleStaff === lastVisibleStaff) { + this.canPaint = false; + return; + } + + this.canPaint = true; + } + public finalizeBracket(smuflMetrics: EngravingSettings) { - // systems with just a single staff do not have a bracket - if (this.firstStaffInBracket === this.lastStaffInBracket) { + if (!this.canPaint) { this.width = 0; return; } @@ -57,12 +92,13 @@ export abstract class SystemBracket { } else { this.width = smuflMetrics.bracketThickness; } - if (!this.drawAsBrace || !this.firstStaffInBracket || !this.lastStaffInBracket) { + + if (!this.drawAsBrace) { return; } - const firstStart: number = this.firstStaffInBracket.contentTop; - const lastEnd: number = this.lastStaffInBracket.contentBottom; + const firstStart: number = this.firstVisibleStaffInBracket!.contentTop; + const lastEnd: number = this.lastVisibleStaffInBracket!.contentBottom; const requiredHeight = lastEnd - firstStart; const requiredScaleForBracket = requiredHeight / bravuraBraceHeightAtMusicFontSize; @@ -77,8 +113,8 @@ export abstract class SystemBracket { class SingleTrackSystemBracket extends SystemBracket { protected track: Track; - public constructor(track: Track) { - super(); + public constructor(system: StaffSystem, track: Track) { + super(system); this.track = track; this.drawAsBrace = SingleTrackSystemBracket.isTrackDrawAsBrace(track); } @@ -118,7 +154,6 @@ class SimilarInstrumentSystemBracket extends SingleTrackSystemBracket { * @internal */ export class StaffSystem { - private _accoladeSpacingCalculated: boolean = false; private _brackets: SystemBracket[] = []; @@ -158,6 +193,7 @@ export class StaffSystem { public topPadding: number; public bottomPadding: number; public allStaves: RenderStaff[] = []; + public firstVisibleStaff?: RenderStaff; public constructor(layout: ScoreLayout) { this.layout = layout; @@ -180,14 +216,48 @@ export class StaffSystem { this.masterBarsRenderers.push(renderers); renderers.layoutingInfo.preBeatSize = 0; let src: number = 0; - for (let i: number = 0, j: number = this.staves.length; i < j; i++) { - const g: StaffTrackGroup = this.staves[i]; - for (let k: number = 0, l: number = g.staves.length; k < l; k++) { - const s: RenderStaff = g.staves[k]; + + let firstVisibleStaff: RenderStaff | undefined = undefined; + let anyStaffVisible = false; + + for (const g of this.staves) { + let firstVisibleStaffInGroup: RenderStaff | undefined = undefined; + let lastVisibleStaffInGroup: RenderStaff | undefined = undefined; + + for (const s of g.staves) { const renderer: BarRendererBase = renderers.renderers[src++]; s.addBarRenderer(renderer); + + if (s.isVisible) { + anyStaffVisible = true; + if (!firstVisibleStaffInGroup) { + firstVisibleStaffInGroup = s; + } + if (!firstVisibleStaff) { + firstVisibleStaff = s; + } + + lastVisibleStaffInGroup = s; + } + } + + g.firstVisibleStaff = firstVisibleStaffInGroup; + g.lastVisibleStaff = lastVisibleStaffInGroup; + if (!firstVisibleStaff) { + firstVisibleStaff = firstVisibleStaffInGroup; } } + + if (!anyStaffVisible) { + const group = this.staves[0]; + const firstStaff = group.staves[0]; + firstStaff.isVisible = true; + group.firstVisibleStaff = firstStaff; + group.lastVisibleStaff = firstStaff; + firstVisibleStaff = firstStaff; + } + + this.firstVisibleStaff = firstVisibleStaff; this._calculateAccoladeSpacing(tracks); this._updateWidthFromLastBar(); @@ -205,9 +275,14 @@ export class StaffSystem { result.masterBar = tracks[0].score.masterBars[barIndex]; this.masterBarsRenderers.push(result); + let firstVisibleStaff: RenderStaff | undefined = undefined; + let anyStaffVisible = false; // add renderers const barLayoutingInfo: BarLayoutingInfo = result.layoutingInfo; for (const g of this.staves) { + let firstVisibleStaffInGroup: RenderStaff | undefined = undefined; + let lastVisibleStaffInGroup: RenderStaff | undefined = undefined; + for (const s of g.staves) { const bar: Bar = g.track.staves[s.modelStaff.index].bars[barIndex]; @@ -217,6 +292,15 @@ export class StaffSystem { : additionalMultiBarRestIndexes.map(b => g.track.staves[s.modelStaff.index].bars[b]); s.addBar(bar, barLayoutingInfo, additionalMultiBarsRestBars); + + if (s.isVisible) { + anyStaffVisible = true; + if (!firstVisibleStaffInGroup) { + firstVisibleStaffInGroup = s; + } + lastVisibleStaffInGroup = s; + } + const renderer: BarRendererBase = s.barRenderers[s.barRenderers.length - 1]; result.renderers.push(renderer); if (renderer.isLinkedToPrevious) { @@ -226,7 +310,24 @@ export class StaffSystem { result.canWrap = false; } } + g.firstVisibleStaff = firstVisibleStaffInGroup; + g.lastVisibleStaff = lastVisibleStaffInGroup; + if (!firstVisibleStaff) { + firstVisibleStaff = firstVisibleStaffInGroup; + } } + + if (!anyStaffVisible) { + const group = this.staves[0]; + const firstStaff = group.staves[0]; + firstStaff.isVisible = true; + group.firstVisibleStaff = firstStaff; + group.lastVisibleStaff = firstStaff; + firstVisibleStaff = firstStaff; + } + + this.firstVisibleStaff = firstVisibleStaff; + this._calculateAccoladeSpacing(tracks); barLayoutingInfo.finish(); @@ -242,19 +343,40 @@ export class StaffSystem { this.masterBarsRenderers.splice(this.masterBarsRenderers.length - 1, 1); let width: number = 0; let barDisplayScale: number = 0; - for (let i: number = 0, j: number = this.allStaves.length; i < j; i++) { - const s: RenderStaff = this.allStaves[i]; - const lastBar: BarRendererBase = s.revertLastBar(); - const computedWidth = lastBar.computedWidth; - if (computedWidth > width) { - width = computedWidth; + + let firstVisibleStaff: RenderStaff | undefined = undefined; + for (const g of this.staves) { + let firstVisibleStaffInGroup: RenderStaff | undefined = undefined; + let lastVisibleStaffInGroup: RenderStaff | undefined = undefined; + + for (const s of g.staves) { + const lastBar: BarRendererBase = s.revertLastBar(); + const computedWidth = lastBar.computedWidth; + if (computedWidth > width) { + width = computedWidth; + } + const newBarDisplayScale = lastBar.barDisplayScale; + if (newBarDisplayScale > barDisplayScale) { + barDisplayScale = newBarDisplayScale; + } + lastBar.afterReverted(); + + if (s.isVisible) { + if (!firstVisibleStaffInGroup) { + firstVisibleStaffInGroup = s; + } + lastVisibleStaffInGroup = s; + } } - const newBarDisplayScale = lastBar.barDisplayScale; - if (newBarDisplayScale > barDisplayScale) { - barDisplayScale = newBarDisplayScale; + + g.firstVisibleStaff = firstVisibleStaffInGroup; + g.lastVisibleStaff = lastVisibleStaffInGroup; + if (!firstVisibleStaff) { + firstVisibleStaff = firstVisibleStaffInGroup; } - lastBar.afterReverted(); } + this.firstVisibleStaff = firstVisibleStaff; + this.width -= width; this.computedWidth -= width; this.totalBarDisplayScale -= barDisplayScale; @@ -385,6 +507,7 @@ export class StaffSystem { let braceWidth = 0; for (const b of this._brackets) { + b.updateCanPaint(); b.finalizeBracket(settings.display.resources.engravingSettings); braceWidth = Math.max(braceWidth, b.width); } @@ -393,6 +516,11 @@ export class StaffSystem { this.width += this.accoladeWidth; this.computedWidth += this.accoladeWidth; + } else { + for (const b of this._brackets) { + b.updateCanPaint(); + b.finalizeBracket(settings.display.resources.engravingSettings); + } } } @@ -426,12 +554,12 @@ export class StaffSystem { break; case BracketExtendMode.GroupStaves: // when grouping staves, we create one bracket for the whole track across all staves - bracket = new SingleTrackSystemBracket(track); + bracket = new SingleTrackSystemBracket(this, track); bracket.index = this._brackets.length; this._brackets.push(bracket); break; case BracketExtendMode.GroupSimilarInstruments: - bracket = new SimilarInstrumentSystemBracket(track); + bracket = new SimilarInstrumentSystemBracket(this, track); bracket.index = this._brackets.length; this._brackets.push(bracket); break; @@ -500,9 +628,12 @@ export class StaffSystem { } public paintPartial(cx: number, cy: number, canvas: ICanvas, startIndex: number, count: number): void { - for (let i: number = 0, j: number = this.allStaves.length; i < j; i++) { - this.allStaves[i].paint(cx, cy, canvas, startIndex, count); + for (const s of this.allStaves) { + if (s.isVisible) { + s.paint(cx, cy, canvas, startIndex, count); + } } + const res: RenderingResources = this.layout.renderer.settings.display.resources; if (this.staves.length > 0 && startIndex === 0) { @@ -551,9 +682,9 @@ export class StaffSystem { const oldBaseLine = canvas.textBaseline; const oldTextAlign = canvas.textAlign; for (const g of this.staves) { - if (g.staves.length > 0) { - const firstStart: number = cy + g.staves[0].contentTop; - const lastEnd: number = cy + g.staves[g.staves.length - 1].contentBottom; + if (g.firstVisibleStaff) { + const firstStart: number = cy + g.firstVisibleStaff.contentTop; + const lastEnd: number = cy + g.lastVisibleStaff!.contentBottom; let trackNameText = ''; switch (trackNameMode) { @@ -617,6 +748,10 @@ export class StaffSystem { if (this.allStaves.length > 0 && needsSystemBarLine) { let previousStaffInBracket: RenderStaff | null = null; for (const s of this.allStaves) { + if (!s.isVisible) { + continue; + } + if (previousStaffInBracket !== null) { const previousBottom = previousStaffInBracket.contentBottom; const thisTop = s.contentTop; @@ -649,11 +784,11 @@ export class StaffSystem { for (const bracket of this._brackets!) { if (bracket.canPaint) { - const barStartX: number = cx + bracket.firstStaffInBracket!.x; + const barStartX: number = cx + bracket.firstVisibleStaffInBracket!.x; const barSize: number = bracket.width; const barOffset: number = settings.display.accoladeBarPaddingRight; - const firstStart: number = cy + bracket.firstStaffInBracket!.contentTop; - const lastEnd: number = cy + bracket.lastStaffInBracket!.contentBottom; + const firstStart: number = cy + bracket.firstVisibleStaffInBracket!.contentTop; + const lastEnd: number = cy + bracket.lastVisibleStaffInBracket!.contentBottom; let accoladeStart: number = firstStart; let accoladeEnd: number = lastEnd; @@ -665,7 +800,7 @@ export class StaffSystem { bracket.braceScale, MusicFontSymbol.Brace ); - } else if (bracket.firstStaffInBracket !== bracket.lastStaffInBracket) { + } else if (bracket.firstVisibleStaffInBracket !== bracket.lastVisibleStaffInBracket) { // brackets typically overflow by 1/4 staff-space const smuflMetrics = settings.display.resources.engravingSettings; @@ -717,46 +852,102 @@ export class StaffSystem { this._hasSystemSeparator = true; } + const anyStaffVisible = this._finalizeTrackGroups(); + + // for now we always force one staff to be visible. + // making also whole systems invisible needs separate attention (also on player cursor handling) + if (!anyStaffVisible) { + const group = this.staves[0]; + const firstStaff = group.staves[0]; + firstStaff.isVisible = true; + this._finalizeTrackGroups(true); + } + + for (const b of this._brackets!) { + b.finalizeBracket(settings.display.resources.engravingSettings); + } + } + + private _finalizeTrackGroups(onlyFirstGroup: boolean = false) { let currentY: number = 0; + const settings = this.layout.renderer.settings; const smufl = settings.display.resources.engravingSettings; const topBracketSpikeHeight = smufl.glyphHeights.get(MusicFontSymbol.BracketTop)!; const bottomBracketSpikeHeight = smufl.glyphHeights.get(MusicFontSymbol.BracketBottom)!; let previousStaff: RenderStaff | undefined = undefined; - for (const staff of this.allStaves) { - // check if we need "in-between padding" - if (previousStaff !== undefined && previousStaff!.trackIndex !== staff.trackIndex) { - currentY += settings.display.trackStaffPaddingBetween; - } + let endSpikeOverflow = 0; + let anyStaffVisible = false; + for (const group of this.staves) { + let firstVisibleStaffInGroup: RenderStaff | undefined = undefined; + let lastVisibleStaffInGroup: RenderStaff | undefined = undefined; + for (const staff of group.staves) { + // check if we need "in-between padding" + if (previousStaff !== undefined && previousStaff!.trackIndex !== staff.trackIndex) { + currentY += settings.display.trackStaffPaddingBetween; + } - const bracket = this._staffToBracket.has(staff) ? this._staffToBracket.get(staff) : undefined; - const hasBracket = bracket && !bracket.drawAsBrace && bracket.canPaint; - if (hasBracket && bracket!.firstStaffInBracket === staff) { - const spikeOverflow = topBracketSpikeHeight - staff.topOverflow; - if (spikeOverflow > 0) { - currentY += spikeOverflow; + const bracket = this._staffToBracket.has(staff) ? this._staffToBracket.get(staff) : undefined; + const hasBracket = bracket && !bracket.drawAsBrace && bracket.canPaint; + if (hasBracket && bracket!.firstStaffInBracket === staff) { + const spikeOverflow = topBracketSpikeHeight - staff.topOverflow; + if (spikeOverflow > 0) { + currentY += spikeOverflow; + } + } + + staff.x = this.accoladeWidth; + staff.y = currentY; + if (!onlyFirstGroup) { + staff.finalizeStaff(); } - } - staff.x = this.accoladeWidth; - staff.y = currentY; - staff.finalizeStaff(); - currentY += staff.height; + if (staff.isVisible) { + currentY += staff.height; - if (hasBracket && bracket!.lastStaffInBracket === staff) { - const spikeOverflow = bottomBracketSpikeHeight - staff.bottomOverflow; - if (spikeOverflow > 0) { - currentY += spikeOverflow; + anyStaffVisible = true; + previousStaff = staff; + + if (!firstVisibleStaffInGroup) { + firstVisibleStaffInGroup = staff; + } + lastVisibleStaffInGroup = staff; + } + + endSpikeOverflow = 0; + if (hasBracket && bracket!.lastStaffInBracket === staff) { + const spikeOverflow = bottomBracketSpikeHeight - staff.bottomOverflow; + if (spikeOverflow > 0) { + if (staff.isVisible) { + currentY += spikeOverflow; + } else { + endSpikeOverflow = spikeOverflow; + } + } } } - previousStaff = staff; + + group.firstVisibleStaff = firstVisibleStaffInGroup; + group.lastVisibleStaff = lastVisibleStaffInGroup; + + if (!this.firstVisibleStaff) { + this.firstVisibleStaff = firstVisibleStaffInGroup; + } + + if (onlyFirstGroup) { + break; + } } - this._contentHeight = currentY; - for (const b of this._brackets!) { - b.finalizeBracket(smufl); + // ensure we add overflow if last bracket is hidden + if (endSpikeOverflow) { + currentY += endSpikeOverflow; } + + this._contentHeight = currentY; + + return anyStaffVisible; } public buildBoundingsLookup(cx: number, cy: number): void { @@ -798,6 +989,9 @@ export class StaffSystem { const masterBarBoundsLookup: Map = new Map(); for (let i: number = 0; i < this.staves.length; i++) { for (const staff of this.staves[i].staves) { + if (!staff.isVisible) { + continue; + } for (const renderer of staff.barRenderers) { let masterBarBounds: MasterBarBounds; if (!masterBarBoundsLookup.has(renderer.bar.masterBar.index)) { diff --git a/packages/alphatab/src/rendering/staves/StaffTrackGroup.ts b/packages/alphatab/src/rendering/staves/StaffTrackGroup.ts index 64bdae57a..0827b231b 100644 --- a/packages/alphatab/src/rendering/staves/StaffTrackGroup.ts +++ b/packages/alphatab/src/rendering/staves/StaffTrackGroup.ts @@ -12,6 +12,8 @@ export class StaffTrackGroup { public track: Track; public staffSystem: StaffSystem; public staves: RenderStaff[] = []; + public firstVisibleStaff?: RenderStaff; + public lastVisibleStaff?: RenderStaff; public bracket: SystemBracket | null = null; public constructor(staffSystem: StaffSystem, track: Track) { diff --git a/packages/alphatab/test-data/musicxml-samples/MozartTrio.png b/packages/alphatab/test-data/musicxml-samples/MozartTrio.png index b95d5c088..9824aed5a 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MozartTrio.png and b/packages/alphatab/test-data/musicxml-samples/MozartTrio.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Saltarello.png b/packages/alphatab/test-data/musicxml-samples/Saltarello.png index 44c3960e9..f9f6368db 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Saltarello.png and b/packages/alphatab/test-data/musicxml-samples/Saltarello.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41c-StaffGroups.png b/packages/alphatab/test-data/musicxml-testsuite/41c-StaffGroups.png index cd947b7c7..bb22d7fcd 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41c-StaffGroups.png and b/packages/alphatab/test-data/musicxml-testsuite/41c-StaffGroups.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.png b/packages/alphatab/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.png index 0b9399eda..4300fb626 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.png and b/packages/alphatab/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/73a-Percussion.png b/packages/alphatab/test-data/musicxml-testsuite/73a-Percussion.png index 4d7b79c2f..2790fe7bf 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/73a-Percussion.png and b/packages/alphatab/test-data/musicxml-testsuite/73a-Percussion.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves-in-first.png b/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves-in-first.png new file mode 100644 index 000000000..602e12303 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves-in-first.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves.png b/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves.png new file mode 100644 index 000000000..abe0f156a Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-hide.png b/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-hide.png new file mode 100644 index 000000000..71681590e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-hide.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-show.png b/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-show.png new file mode 100644 index 000000000..2d14ecc48 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-show.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/clefs.png b/packages/alphatab/test-data/visual-tests/music-notation/clefs.png index baf6b2575..71185bf83 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/clefs.png and b/packages/alphatab/test-data/visual-tests/music-notation/clefs.png differ diff --git a/packages/alphatab/test/importer/AlphaTexImporter.test.ts b/packages/alphatab/test/importer/AlphaTexImporter.test.ts index 90586ed74..8c389a2a9 100644 --- a/packages/alphatab/test/importer/AlphaTexImporter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexImporter.test.ts @@ -2514,7 +2514,6 @@ describe('AlphaTexImporterTest', () => { ).toMatchSnapshot(); }); it('barWise', () => { - expect( parseTex(` \\voiceMode barWise @@ -2530,7 +2529,7 @@ describe('AlphaTexImporterTest', () => { }); }); - it('inline-chord-diagrams', () =>{ + it('inline-chord-diagrams', () => { let score = parseTex(` \\chordDiagramsInScore \\chord ("E" 0 0 1 2 2 0) @@ -2551,5 +2550,35 @@ describe('AlphaTexImporterTest', () => { (0.1 0.2 1.3 2.4 2.5 0.6){ch "E"} `); expect(score.stylesheet.globalDisplayChordDiagramsInScore).to.be.false; - }) + }); + + it('empty-staff-options', () => { + let score = parseTex(` + \\hideEmptyStaves + C4 + `); + expect(score.stylesheet.hideEmptyStaves).to.be.true; + expect(score.stylesheet.hideEmptyStavesInFirstSystem).to.be.false; + expect(score.stylesheet.showSingleStaffBrackets).to.be.false; + + score = parseTex(` + \\hideEmptyStaves + \\hideEmptyStavesInFirstSystem + `); + expect(score.stylesheet.hideEmptyStaves).to.be.true; + expect(score.stylesheet.hideEmptyStavesInFirstSystem).to.be.true; + + score = parseTex(` + \\hideEmptyStavesInFirstSystem + C4 + `); + expect(score.stylesheet.hideEmptyStaves).to.be.false; + expect(score.stylesheet.hideEmptyStavesInFirstSystem).to.be.true; + + score = parseTex(` + \\showSingleStaffBrackets + C4 + `); + expect(score.stylesheet.showSingleStaffBrackets).to.be.true; + }); }); diff --git a/packages/alphatab/test/visualTests/features/Layout.test.ts b/packages/alphatab/test/visualTests/features/Layout.test.ts index 94ed0628f..8b6b9551f 100644 --- a/packages/alphatab/test/visualTests/features/Layout.test.ts +++ b/packages/alphatab/test/visualTests/features/Layout.test.ts @@ -208,4 +208,125 @@ describe('LayoutTests', () => { } ); }); + + it('hide-empty-staves', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\hideEmptyStaves + \\defaultSystemsLayout 3 + \\multiTrackTrackNamePolicy allSystems + \\track "T1" + C4.4 *4 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + + \\track "T2" + \\clef C3 + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + `, + 'test-data/visual-tests/layout/hide-empty-staves.png', + undefined, + o => { + o.tracks = o.score.tracks.map(t => t.index); + o.settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + } + ); + }); + + it('hide-empty-staves-in-first', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\hideEmptyStaves + \\hideEmptyStavesInFirstSystem + + \\defaultSystemsLayout 3 + \\multiTrackTrackNamePolicy allSystems + \\track "T1" + C4.4 *4 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + + \\track "T2" + \\clef C3 + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + `, + 'test-data/visual-tests/layout/hide-empty-staves-in-first.png', + undefined, + o => { + o.tracks = o.score.tracks.map(t => t.index); + o.settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + } + ); + }); + + it('single-staff-brackets', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\hideEmptyStaves + \\showSingleStaffBrackets + \\defaultSystemsLayout 3 + \\multiTrackTrackNamePolicy allSystems + \\track "T1" + \\staff {score} + C4.4 *4 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + \\staff {score} + \\clef C3 + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + `, + 'test-data/visual-tests/layout/single-staff-brackets-show.png', + undefined, + o => { + o.tracks = o.score.tracks.map(t => t.index); + o.settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + } + ); + + await VisualTestHelper.runVisualTestTex( + ` + \\hideEmptyStaves + \\defaultSystemsLayout 3 + \\multiTrackTrackNamePolicy allSystems + \\track "T1" + \\staff {score} + C4.4 *4 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + \\staff {score} + \\clef C3 + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + `, + 'test-data/visual-tests/layout/single-staff-brackets-hide.png', + undefined, + o => { + o.tracks = o.score.tracks.map(t => t.index); + o.settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + } + ); + }); }); diff --git a/packages/alphatex/src/definitions.ts b/packages/alphatex/src/definitions.ts index 0ffb4163d..979b4c77a 100644 --- a/packages/alphatex/src/definitions.ts +++ b/packages/alphatex/src/definitions.ts @@ -150,6 +150,9 @@ import { db } from '@coderline/alphatab-alphatex/metadata/bar/db'; import { voiceMode } from '@coderline/alphatab-alphatex/metadata/bar/voiceMode'; import { chordDiagramsInScore } from '@coderline/alphatab-alphatex/metadata/score/chordDiagramsInScore'; import { extendBarLines } from '@coderline/alphatab-alphatex/metadata/score/extendbarlines'; +import { hideEmptyStaves } from '@coderline/alphatab-alphatex/metadata/score/hideemptystaves'; +import { hideEmptyStavesInFirstSystem } from '@coderline/alphatab-alphatex/metadata/score/hideemptystavesinfirstsystem'; +import { showSingleStaffBrackets } from '@coderline/alphatab-alphatex/metadata/score/showsinglestaffbrackets'; import { instrumentMeta } from '@coderline/alphatab-alphatex/metadata/staff/instrument'; import type { AlphaTexExample, WithDescription, WithSignatures } from '@coderline/alphatab-alphatex/types'; @@ -181,7 +184,10 @@ export const scoreMetaData = metadata( firstSystemTrackNameOrientation, otherSystemsTrackNameOrientation, extendBarLines, - chordDiagramsInScore + chordDiagramsInScore, + hideEmptyStaves, + hideEmptyStavesInFirstSystem, + showSingleStaffBrackets ); export const staffMetaData = metadata( diff --git a/packages/alphatex/src/metadata/score/hideemptystaves.ts b/packages/alphatex/src/metadata/score/hideemptystaves.ts new file mode 100644 index 000000000..133de18d7 --- /dev/null +++ b/packages/alphatex/src/metadata/score/hideemptystaves.ts @@ -0,0 +1,25 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const hideEmptyStaves: MetadataTagDefinition = { + tag: '\\hideEmptyStaves', + snippet: '\\hideEmptyStaves', + shortDescription: `Hide empty staves.`, + signatures: [ + { + parameters: [] + } + ], + examples: { + options: { display: {systemsLayoutMode: 'UseModelLayout'}}, + tex: ` + \\hideEmptyStaves + \\defaultSystemsLayout 3 + \\track "Track 1" + C4 * 4 | C4 * 4 | C4 * 4 | + C4 * 4 | C4 * 4 | C4 * 4 | + \\track "Track 2" + r | r | r | + r | C4.1 | r | + ` + } +} diff --git a/packages/alphatex/src/metadata/score/hideemptystavesinfirstsystem.ts b/packages/alphatex/src/metadata/score/hideemptystavesinfirstsystem.ts new file mode 100644 index 000000000..2274a306f --- /dev/null +++ b/packages/alphatex/src/metadata/score/hideemptystavesinfirstsystem.ts @@ -0,0 +1,26 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const hideEmptyStavesInFirstSystem: MetadataTagDefinition = { + tag: '\\hideEmptyStavesInFirstSystem', + snippet: '\\hideEmptyStavesInFirstSystem', + shortDescription: `Hide empty staves in first system.`, + signatures: [ + { + parameters: [] + } + ], + examples: { + options: { display: {systemsLayoutMode: 'UseModelLayout'}}, + tex: ` + \\hideEmptyStaves + \\hideEmptyStavesInFirstSystem + \\defaultSystemsLayout 3 + \\track "Track 1" + C4 * 4 | C4 * 4 | C4 * 4 | + r | r | r | + \\track "Track 2" + r | r | r | + r | C4.1 | r | + ` + } +} diff --git a/packages/alphatex/src/metadata/score/showsinglestaffbrackets.ts b/packages/alphatex/src/metadata/score/showsinglestaffbrackets.ts new file mode 100644 index 000000000..40c4d4bc3 --- /dev/null +++ b/packages/alphatex/src/metadata/score/showsinglestaffbrackets.ts @@ -0,0 +1,28 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const showSingleStaffBrackets: MetadataTagDefinition = { + tag: '\\showSingleStaffBrackets', + snippet: '\\showSingleStaffBrackets', + shortDescription: `Show brackets and braces on single staves.`, + signatures: [ + { + parameters: [] + } + ], + examples: { + options: { display: {systemsLayoutMode: 'UseModelLayout'}}, + tex: ` + \\hideEmptyStaves + \\hideEmptyStavesInFirstSystem + \\showSingleStaffBrackets + \\defaultSystemsLayout 3 + \\track "Track 1" + \\staff { score } + C4 * 4 | C4 * 4 | C4 * 4 | + C4 * 4 | C4 * 4 | C4 * 4 | + \\staff { score } + r | r | r | + r | C4.1 | r | + ` + } +}