diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Set.cs b/src.csharp/AlphaTab/Core/EcmaScript/Set.cs new file mode 100644 index 000000000..0a8a0f324 --- /dev/null +++ b/src.csharp/AlphaTab/Core/EcmaScript/Set.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace AlphaTab.Core.EcmaScript +{ + public class Set + { + private readonly HashSet _data = new HashSet(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) + { + _data.Add(item); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Has(T item) + { + return _data.Contains(item); + } + + public void ForEach(Action action) + { + foreach (var i in _data) + { + action(i); + } + } + } +} diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index 046d30950..37da485d0 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -969,8 +969,8 @@ export class AlphaTabApiBase { if (this.settings.player.enableUserInteraction) { // for the selection ensure start < end if (this._selectionEnd) { - let startTick: number = this._selectionStart!.beat.absoluteDisplayStart; - let endTick: number = this._selectionStart!.beat.absoluteDisplayStart; + let startTick: number = this._selectionStart!.beat.absolutePlaybackStart; + let endTick: number = this._selectionStart!.beat.absolutePlaybackStart; if (endTick < startTick) { let t: SelectionInfo = this._selectionStart!; this._selectionStart = this._selectionEnd; diff --git a/src/model/Beat.ts b/src/model/Beat.ts index 196b70d12..779fdf8b3 100644 --- a/src/model/Beat.ts +++ b/src/model/Beat.ts @@ -22,6 +22,7 @@ import { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { BeatCloner } from '@src/generated/model/BeatCloner'; +import { GraceGroup } from './GraceGroup'; /** * Lists the different modes on how beaming for a beat should be done. @@ -324,6 +325,22 @@ export class Beat { */ public graceType: GraceType = GraceType.None; + /** + * Gets or sets the grace group this beat belongs to. + * If this beat is not a grace note, it holds the group which belongs to this beat. + * @json_ignore + * @clone_ignore + */ + public graceGroup: GraceGroup | null = null; + + /** + * Gets or sets the index of this beat within the grace group if + * this is a grace beat. + * @json_ignore + * @clone_ignore + */ + public graceIndex: number = -1; + /** * Gets or sets the pickstroke applied on this beat. */ @@ -507,7 +524,7 @@ export class Beat { public updateDurations(): void { let ticks: number = this.calculateDuration(); this.playbackDuration = ticks; - this.displayDuration = ticks; + switch (this.graceType) { case GraceType.BeforeBeat: case GraceType.OnBeat: @@ -522,20 +539,17 @@ export class Beat { this.playbackDuration = MidiUtils.toTicks(Duration.ThirtySecond); break; } + this.displayDuration = 0; break; case GraceType.BendGrace: this.playbackDuration /= 2; + this.displayDuration = 0; break; default: + this.displayDuration = ticks; let previous: Beat | null = this.previousBeat; if (previous && previous.graceType === GraceType.BendGrace) { this.playbackDuration = previous.playbackDuration; - } else { - while (previous && previous.graceType === GraceType.OnBeat) { - // if the previous beat is a on-beat grace it steals the duration from this beat - this.playbackDuration -= previous.playbackDuration; - previous = previous.previousBeat; - } } break; } @@ -562,6 +576,22 @@ export class Beat { this.automations.push(Automation.buildInstrumentAutomation(false, 0, this.voice.bar.staff.track.playbackInfo.program)); } + switch (this.graceType) { + case GraceType.OnBeat: + case GraceType.BeforeBeat: + let numberOfGraceBeats: number = this.graceGroup!.beats.length; + // set right duration for beaming/display + if (numberOfGraceBeats === 1) { + this.duration = Duration.Eighth; + } else if (numberOfGraceBeats === 2) { + this.duration = Duration.Sixteenth; + } else { + this.duration = Duration.ThirtySecond; + } + break; + } + + let displayMode: NotationMode = !settings ? NotationMode.GuitarPro : settings.notation.notationMode; let isGradual: boolean = this.text === 'grad' || this.text === 'grad.'; if (isGradual && displayMode === NotationMode.SongBook) { @@ -759,6 +789,9 @@ export class Beat { cloneNote.isTieDestination = true; } this.graceType = GraceType.BendGrace; + this.graceGroup = new GraceGroup(); + this.graceGroup.addBeat(this); + this.graceGroup.isComplete = true; this.updateDurations(); this.voice.insertBeat(this, cloneBeat); } diff --git a/src/model/GraceGroup.ts b/src/model/GraceGroup.ts new file mode 100644 index 000000000..db6aae5d4 --- /dev/null +++ b/src/model/GraceGroup.ts @@ -0,0 +1,35 @@ +import { Beat } from './Beat'; + +/** + * Represents a group of grace beats that belong together + */ +export class GraceGroup { + /** + * All beats within this group. + */ + public beats: Beat[] = []; + + /** + * Gets a unique ID for this grace group. + */ + public id: string = 'empty'; + + /** + * true if the grace beat are followed by a normal beat within the same + * bar. + */ + public isComplete: boolean = false; + + /** + * Adds a new beat to this group + * @param beat The beat to add + */ + public addBeat(beat: Beat) { + if (this.beats.length === 0) { + this.id = beat.absoluteDisplayStart + "_" + beat.voice.index; + } + beat.graceIndex = this.beats.length; + beat.graceGroup = this; + this.beats.push(beat); + } +} diff --git a/src/model/Voice.ts b/src/model/Voice.ts index 4afbec081..0fe5bd0de 100644 --- a/src/model/Voice.ts +++ b/src/model/Voice.ts @@ -1,9 +1,8 @@ -import { MidiUtils } from '@src/midi/MidiUtils'; import { Bar } from '@src/model/Bar'; import { Beat } from '@src/model/Beat'; -import { Duration } from '@src/model/Duration'; import { GraceType } from '@src/model/GraceType'; import { Settings } from '@src/Settings'; +import { GraceGroup } from './GraceGroup'; /** * A voice represents a group of beats @@ -98,90 +97,107 @@ export class Voice { this.isEmpty = false; } - public getBeatAtDisplayStart(displayStart: number): Beat | null { - if (this._beatLookup.has(displayStart)) { - return this._beatLookup.get(displayStart)!; + public getBeatAtPlaybackStart(playbackStart: number): Beat | null { + if (this._beatLookup.has(playbackStart)) { + return this._beatLookup.get(playbackStart)!; } return null; } public finish(settings: Settings): void { this._beatLookup = new Map(); + let currentGraceGroup: GraceGroup | null = null; for (let index: number = 0; index < this.beats.length; index++) { let beat: Beat = this.beats[index]; beat.index = index; this.chain(beat); + if (beat.graceType === GraceType.None) { + beat.graceGroup = currentGraceGroup; + if (currentGraceGroup) { + currentGraceGroup.isComplete = true; + } + currentGraceGroup = null; + } else { + if (!currentGraceGroup) { + currentGraceGroup = new GraceGroup(); + } + currentGraceGroup.addBeat(beat); + } } + let currentDisplayTick: number = 0; let currentPlaybackTick: number = 0; for (let i: number = 0; i < this.beats.length; i++) { let beat: Beat = this.beats[i]; beat.index = i; beat.finish(settings); - if (beat.graceType === GraceType.None || beat.graceType === GraceType.BendGrace) { - beat.displayStart = currentDisplayTick; - beat.playbackStart = currentPlaybackTick; - currentDisplayTick += beat.displayDuration; - currentPlaybackTick += beat.playbackDuration; - } else { - if (!beat.previousBeat || beat.previousBeat.graceType === GraceType.None) { - // find note which is not a grace note - let nonGrace: Beat | null = beat; - let numberOfGraceBeats: number = 0; - while (nonGrace && nonGrace.graceType !== GraceType.None) { - nonGrace = nonGrace.nextBeat; - numberOfGraceBeats++; - } - let graceDuration: Duration = Duration.Eighth; - let stolenDuration: number = 0; - if (numberOfGraceBeats === 1) { - graceDuration = Duration.Eighth; - } else if (numberOfGraceBeats === 2) { - graceDuration = Duration.Sixteenth; - } else { - graceDuration = Duration.ThirtySecond; - } - if (nonGrace) { - nonGrace.updateDurations(); - } - // grace beats have 1/4 size of the non grace beat preceeding them - let perGraceDisplayDuration: number = !beat.previousBeat - ? MidiUtils.toTicks(Duration.ThirtySecond) - : (((beat.previousBeat.displayDuration / 4) | 0) / numberOfGraceBeats) | 0; - // move all grace beats - let graceBeat: Beat | null = this.beats[i]; - for (let j: number = 0; j < numberOfGraceBeats && graceBeat; j++) { - graceBeat.duration = graceDuration; - graceBeat.updateDurations(); - graceBeat.displayStart = - currentDisplayTick - (numberOfGraceBeats - j + 1) * perGraceDisplayDuration; - graceBeat.displayDuration = perGraceDisplayDuration; - stolenDuration += graceBeat.playbackDuration; - graceBeat = graceBeat.nextBeat; - } - // steal needed duration from beat duration - if (beat.graceType === GraceType.BeforeBeat) { - if (beat.previousBeat) { - beat.previousBeat.playbackDuration -= stolenDuration; + + // if this beat is a non-grace but has grace notes + // we need to first steal the duration from the right beat + // and place the grace beats correctly + if (beat.graceType === GraceType.None) { + if (beat.graceGroup) { + const firstGraceBeat = beat.graceGroup!.beats[0]; + const lastGraceBeat = beat.graceGroup!.beats[beat.graceGroup!.beats.length - 1]; + if (firstGraceBeat.graceType !== GraceType.BendGrace) { + // find out the stolen duration first + let stolenDuration: number = (lastGraceBeat.playbackStart + lastGraceBeat.playbackDuration) - firstGraceBeat.playbackStart; + + switch (firstGraceBeat.graceType) { + case GraceType.BeforeBeat: + // steal duration from previous beat and then place grace beats newly + if (firstGraceBeat.previousBeat) { + firstGraceBeat.previousBeat.playbackDuration -= stolenDuration; + // place beats starting after new beat end + if (firstGraceBeat.previousBeat.voice == this) { + currentPlaybackTick = firstGraceBeat.previousBeat.playbackStart + + firstGraceBeat.previousBeat.playbackDuration; + } else { + // stealing into the previous bar + currentPlaybackTick = -stolenDuration; + } + } else { + // before-beat on start is somehow not possible as it causes negative ticks + currentPlaybackTick = -stolenDuration; + } + + for (const graceBeat of beat.graceGroup!.beats) { + this._beatLookup.delete(graceBeat.playbackStart); + graceBeat.playbackStart = currentPlaybackTick; + this._beatLookup.set(graceBeat.playbackStart, beat); + currentPlaybackTick += graceBeat.playbackDuration; + } + + break; + case GraceType.OnBeat: + // steal duration from current beat + beat.playbackDuration -= stolenDuration; + if (lastGraceBeat.voice === this) { + // with changed durations, update current position to be after the last grace beat + currentPlaybackTick = lastGraceBeat.playbackStart + lastGraceBeat.playbackDuration; + } else { + // if last grace beat is on the previous bar, we shift the time back to have the note played earlier + currentPlaybackTick = -stolenDuration; + } + break; } - currentPlaybackTick -= stolenDuration; - } else if (nonGrace && beat.graceType === GraceType.OnBeat) { - nonGrace.playbackDuration -= stolenDuration; } } - beat.playbackStart = currentPlaybackTick; - currentPlaybackTick = beat.playbackStart + beat.playbackDuration; - } + if (beat.fermata) { + this.bar.masterBar.addFermata(beat.playbackStart, beat.fermata); + } else { + beat.fermata = this.bar.masterBar.getFermata(beat); + } - if(beat.fermata) { - this.bar.masterBar.addFermata(beat.playbackStart, beat.fermata); - } else { - beat.fermata = this.bar.masterBar.getFermata(beat); + this._beatLookup.set(beat.playbackStart, beat); } + beat.displayStart = currentDisplayTick; + beat.playbackStart = currentPlaybackTick; beat.finishTuplet(); - this._beatLookup.set(beat.displayStart, beat); + currentDisplayTick += beat.displayDuration; + currentPlaybackTick += beat.playbackDuration; } } diff --git a/src/rendering/ScoreBarRenderer.ts b/src/rendering/ScoreBarRenderer.ts index 29038cfd0..1385af456 100644 --- a/src/rendering/ScoreBarRenderer.ts +++ b/src/rendering/ScoreBarRenderer.ts @@ -835,7 +835,6 @@ export class ScoreBarRenderer extends BarRendererBase { } } - // TODO[performance]: Maybe we should cache this (check profiler) public getNoteLine(n: Note): number { return this.accidentalHelper.getNoteLine(n); } diff --git a/src/rendering/effects/DynamicsEffectInfo.ts b/src/rendering/effects/DynamicsEffectInfo.ts index 50dfe0d15..7930d55a0 100644 --- a/src/rendering/effects/DynamicsEffectInfo.ts +++ b/src/rendering/effects/DynamicsEffectInfo.ts @@ -43,7 +43,7 @@ export class DynamicsEffectInfo extends EffectBarRendererInfo { if (show && beat.voice.index > 0) { for (let voice of beat.voice.bar.voices) { if (voice.index < beat.voice.index) { - let beatAtSamePos = voice.getBeatAtDisplayStart(beat.displayStart); + let beatAtSamePos = voice.getBeatAtPlaybackStart(beat.playbackStart); if ( beatAtSamePos && beat.dynamics === beatAtSamePos.dynamics && diff --git a/src/rendering/glyphs/BeatContainerGlyph.ts b/src/rendering/glyphs/BeatContainerGlyph.ts index dcea65044..db283abdf 100644 --- a/src/rendering/glyphs/BeatContainerGlyph.ts +++ b/src/rendering/glyphs/BeatContainerGlyph.ts @@ -1,5 +1,6 @@ import { Beat } from '@src/model/Beat'; import { Duration } from '@src/model/Duration'; +import { GraceType } from '@src/model/GraceType'; import { Note } from '@src/model/Note'; import { ICanvas } from '@src/platform/ICanvas'; import { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; @@ -11,8 +12,10 @@ import { BarBounds } from '../utils/BarBounds'; import { BeatBounds } from '../utils/BeatBounds'; import { Bounds } from '../utils/Bounds'; import { FlagGlyph } from './FlagGlyph'; +import { NoteHeadGlyph } from './NoteHeadGlyph'; export class BeatContainerGlyph extends Glyph { + public static readonly GraceBeatPadding:number = 3; public voiceContainer: VoiceContainerGlyph; public beat: Beat; public preNotes!: BeatGlyphBase; @@ -33,16 +36,25 @@ export class BeatContainerGlyph extends Glyph { public registerLayoutingInfo(layoutings: BarLayoutingInfo): void { let preBeatStretch: number = this.onTimeX; + if(this.beat.graceGroup && !this.beat.graceGroup.isComplete) { + preBeatStretch += BeatContainerGlyph.GraceBeatPadding * this.renderer.scale; + } + let postBeatStretch: number = this.onNotes.width - this.onNotes.centerX; // make space for flag const helper = this.renderer.helpers.getBeamingHelperForBeat(this.beat); - if(helper && helper.hasFlag) { - postBeatStretch += FlagGlyph.FlagWidth * this.scale; + 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) { postBeatStretch += tie.width; } + // Add some further spacing to grace notes + if(this.beat.graceType !== GraceType.None) { + postBeatStretch += BeatContainerGlyph.GraceBeatPadding * this.renderer.scale; + } + layoutings.addBeatSpring(this.beat, preBeatStretch, postBeatStretch); // store sizes for special renderers like the EffectBarRenderer layoutings.setPreBeatSize(this.beat, this.preNotes.width); @@ -52,11 +64,16 @@ 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) { + offset += BeatContainerGlyph.GraceBeatPadding * this.renderer.scale; + } + this.preNotes.x = offset; this.preNotes.width = info.getPreBeatSize(this.beat); this.onNotes.width = info.getOnBeatSize(this.beat); this.onNotes.x = this.preNotes.x + this.preNotes.width; this.onNotes.updateBeamingHelper(); + this.updateWidth(); } public doLayout(): void { @@ -132,7 +149,7 @@ export class BeatContainerGlyph extends Glyph { // var ta = canvas.textAlign; // canvas.color = new Color(255, 0, 0); // canvas.textAlign = TextAlign.Left; - // canvas.fillText(this.beat.displayStart.toString(), cx + this.x, cy + this.y - 10); + // canvas.fillText(this.beat.playbackStart.toString(), cx + this.x, cy + this.y - 10); // canvas.color = c; // canvas.textAlign = ta; // canvas.color = Color.random(); diff --git a/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts b/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts index a37ceff9b..a6a0b3c77 100644 --- a/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts +++ b/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts @@ -28,6 +28,7 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { let accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); let ghost: GhostNoteContainerGlyph = new GhostNoteContainerGlyph(true); ghost.renderer = this.renderer; + this._prebends = new BendNoteHeadGroupGlyph(this.container.beat, true); this._prebends.renderer = this.renderer; for (let note of this.container.beat.notes) { diff --git a/src/rendering/glyphs/VoiceContainerGlyph.ts b/src/rendering/glyphs/VoiceContainerGlyph.ts index 6906037db..1b526042f 100644 --- a/src/rendering/glyphs/VoiceContainerGlyph.ts +++ b/src/rendering/glyphs/VoiceContainerGlyph.ts @@ -1,3 +1,4 @@ +import { GraceType } from '@src/model/GraceType'; import { TupletGroup } from '@src/model/TupletGroup'; import { Voice } from '@src/model/Voice'; import { ICanvas } from '@src/platform/ICanvas'; @@ -34,11 +35,55 @@ export class VoiceContainerGlyph extends GlyphGroup { this.width = this.renderer.layoutingInfo.calculateVoiceWidth(force) * scale; let positions: Map = this.renderer.layoutingInfo.buildOnTimePositions(force); let beatGlyphs: BeatContainerGlyph[] = this.beatGlyphs; + for (let i: number = 0, j: number = beatGlyphs.length; i < j; i++) { let currentBeatGlyph: BeatContainerGlyph = beatGlyphs[i]; - let time: number = currentBeatGlyph.beat.absoluteDisplayStart; - currentBeatGlyph.x = positions.get(time)! * scale - currentBeatGlyph.onTimeX; - // size always previousl glyph after we know the position + + switch (currentBeatGlyph.beat.graceType) { + case GraceType.None: + currentBeatGlyph.x = positions.get(currentBeatGlyph.beat.absoluteDisplayStart)! * scale - currentBeatGlyph.onTimeX; + break; + default: + const graceDisplayStart = currentBeatGlyph.beat.graceGroup!.beats[0].absoluteDisplayStart; + const graceGroupId = currentBeatGlyph.beat.graceGroup!.id; + // placement for proper grace notes which have a following note + if (currentBeatGlyph.beat.graceGroup!.isComplete && positions.has(graceDisplayStart)) { + currentBeatGlyph.x = positions.get(graceDisplayStart)! * scale - currentBeatGlyph.onTimeX; + let graceSprings = this.renderer.layoutingInfo.allGraceRods.get(graceGroupId)!; + let graceTargetPreBeat = this.renderer.layoutingInfo.springs.get(graceDisplayStart)!.preBeatWidth; + // move right in front to the note + currentBeatGlyph.x -= graceTargetPreBeat; + // respect the post beat width of the grace note + currentBeatGlyph.x -= graceSprings[currentBeatGlyph.beat.graceIndex].postSpringWidth; + // shift to right position of the particular grace note + currentBeatGlyph.x += graceSprings[currentBeatGlyph.beat.graceIndex].graceBeatWidth; + } else { + // placement for improper grace beats where no beat in the same bar follows + let graceSpring = this.renderer.layoutingInfo.incompleteGraceRods.get(graceGroupId)!; + const relativeOffset = graceSpring[currentBeatGlyph.beat.graceIndex].postSpringWidth + - graceSpring[currentBeatGlyph.beat.graceIndex].preSpringWidth + + if (i > 0) { + if (currentBeatGlyph.beat.graceIndex === 0) { + // we place the grace beat directly after the previous one + // otherwise this causes flickers on resizing + currentBeatGlyph.x = beatGlyphs[i - 1].x + beatGlyphs[i - 1].width; + } else { + // for the multiple grace glyphs we take the width of the grace rod + // this width setting is aligned with the positioning logic below + currentBeatGlyph.x = beatGlyphs[i - 1].x + + graceSpring[currentBeatGlyph.beat.graceIndex - 1].postSpringWidth + - graceSpring[currentBeatGlyph.beat.graceIndex - 1].preSpringWidth + - relativeOffset; + } + } else { + currentBeatGlyph.x = -relativeOffset; + } + } + break; + } + + // size always previous glyph after we know the position // of the next glyph if (i > 0) { let beatWidth: number = currentBeatGlyph.x - beatGlyphs[i - 1].x; diff --git a/src/rendering/staves/BarLayoutingInfo.ts b/src/rendering/staves/BarLayoutingInfo.ts index 4240b13b9..50ce6c04f 100644 --- a/src/rendering/staves/BarLayoutingInfo.ts +++ b/src/rendering/staves/BarLayoutingInfo.ts @@ -4,6 +4,7 @@ import { Duration } from '@src/model/Duration'; import { Spring } from '@src/rendering/staves/Spring'; import { ModelUtils } from '@src/model/ModelUtils'; import { ICanvas } from '@src/platform/ICanvas'; +import { GraceType } from '@src/model/GraceType'; /** * This public class stores size information about a stave. @@ -19,6 +20,7 @@ export class BarLayoutingInfo { private _minTime: number = -1; private _onTimePositionsForce: number = 0; private _onTimePositions: Map = new Map(); + private _incompleteGraceRodsWidth: number = 0; /** * an internal version number that increments whenever a change was made. @@ -89,30 +91,33 @@ export class BarLayoutingInfo { } } + public incompleteGraceRods: Map = new Map(); + public allGraceRods: Map = new Map(); public springs: Map = new Map(); - public addSpring(start: number, duration: number, preSpringSize: number, postSpringSize: number): Spring { + public addSpring(start: number, duration: number, graceBeatWidth: number, preBeatWidth: number, postSpringSize: number): Spring { this.version++; let spring: Spring; if (!this.springs.has(start)) { spring = new Spring(); spring.timePosition = start; - spring.allDurations.push(duration); + spring.allDurations.add(duration); // check in the previous spring for the shortest duration that overlaps with this spring // Gourlay defines that we need the smallest note duration that either starts **or continues** on the current spring. if (this._timeSortedSprings.length > 0) { let smallestDuration: number = duration; let previousSpring: Spring = this._timeSortedSprings[this._timeSortedSprings.length - 1]; - for (let prevDuration of previousSpring.allDurations) { + previousSpring.allDurations.forEach(prevDuration => { let end: number = previousSpring.timePosition + prevDuration; if (end >= start && prevDuration < smallestDuration) { smallestDuration = prevDuration; } - } + }); } spring.longestDuration = duration; spring.postSpringWidth = postSpringSize; - spring.preSpringWidth = preSpringSize; + spring.graceBeatWidth = graceBeatWidth; + spring.preBeatWidth = preBeatWidth; this.springs.set(start, spring); let timeSorted: Spring[] = this._timeSortedSprings; let insertPos: number = timeSorted.length - 1; @@ -125,8 +130,11 @@ export class BarLayoutingInfo { if (spring.postSpringWidth < postSpringSize) { spring.postSpringWidth = postSpringSize; } - if (spring.preSpringWidth < preSpringSize) { - spring.preSpringWidth = preSpringSize; + if (spring.graceBeatWidth < graceBeatWidth) { + spring.graceBeatWidth = graceBeatWidth; + } + if (spring.preBeatWidth < preBeatWidth) { + spring.preBeatWidth = preBeatWidth; } if (duration < spring.smallestDuration) { spring.smallestDuration = duration; @@ -134,7 +142,7 @@ export class BarLayoutingInfo { if (duration > spring.longestDuration) { spring.longestDuration = duration; } - spring.allDurations.push(duration); + spring.allDurations.add(duration); } if (this._minTime === -1 || this._minTime > start) { this._minTime = start; @@ -142,12 +150,78 @@ export class BarLayoutingInfo { return spring; } - public addBeatSpring(beat: Beat, preBeatSize: number, postBeatSize: number): Spring { + public addBeatSpring(beat: Beat, preBeatSize: number, postBeatSize: number): void { let start: number = beat.absoluteDisplayStart; - return this.addSpring(start, beat.displayDuration, preBeatSize, postBeatSize); + if (beat.graceType !== GraceType.None) { + // For grace beats we just remember the the sizes required for them + // these sizes are then considered when the target beat is added. + + const groupId = beat.graceGroup!.id; + + if (!this.allGraceRods.has(groupId)) { + this.allGraceRods.set(groupId, new Array(beat.graceGroup!.beats.length)); + } + + if (!beat.graceGroup!.isComplete && !this.incompleteGraceRods.has(groupId)) { + this.incompleteGraceRods.set(groupId, new Array(beat.graceGroup!.beats.length)); + } + + let existingSpring = this.allGraceRods.get(groupId)![beat.graceIndex]; + if (existingSpring) { + if (existingSpring.postSpringWidth < postBeatSize) { + existingSpring.postSpringWidth = postBeatSize; + } + if (existingSpring.preBeatWidth < preBeatSize) { + existingSpring.preBeatWidth = preBeatSize; + } + } else { + const graceSpring = new Spring(); + graceSpring.timePosition = start; + graceSpring.postSpringWidth = postBeatSize; + graceSpring.preBeatWidth = preBeatSize; + if (!beat.graceGroup!.isComplete) { + this.incompleteGraceRods.get(groupId)![beat.graceIndex] = graceSpring; + } + this.allGraceRods.get(groupId)![beat.graceIndex] = graceSpring; + } + } else { + let graceBeatSize = 0; + if (beat.graceGroup && this.allGraceRods.has(beat.graceGroup.id)) { + for (const graceBeat of this.allGraceRods.get(beat.graceGroup.id)!) { + graceBeatSize += graceBeat.springWidth; + } + } + + this.addSpring(start, beat.displayDuration, graceBeatSize, preBeatSize, postBeatSize); + } } public finish(): void { + this.allGraceRods.forEach((s, k) => { + let offset = 0; + if (this.incompleteGraceRods.has(k)) { + for (const sp of s) { + offset += sp.preBeatWidth; + sp.graceBeatWidth = offset; + offset += sp.postSpringWidth; + } + } else { + for (let i = s.length - 1; i >= 0; i--) { + // for grace beats we store the offset + // in the 'graceBeatWidth' for later use during applying + // beat positions + s[i].graceBeatWidth = offset; + offset -= (s[i].preBeatWidth + s[i].postSpringWidth); + } + } + }); + this._incompleteGraceRodsWidth = 0; + this.incompleteGraceRods.forEach(s => { + for (const sp of s) { + this._incompleteGraceRodsWidth += sp.preBeatWidth + sp.postSpringWidth; + } + }); + this.calculateSpringConstants(); this.version++; } @@ -162,6 +236,11 @@ export class BarLayoutingInfo { }); let totalSpringConstant: number = 0; let sortedSprings: Spring[] = this._timeSortedSprings; + if (sortedSprings.length === 0) { + this.totalSpringConstant = -1; + this.minStretchForce = -1; + return; + } for (let i: number = 0; i < sortedSprings.length; i++) { let currentSpring: Spring = sortedSprings[i]; let duration: number = 0; @@ -205,7 +284,7 @@ export class BarLayoutingInfo { } public height: number = 0; - public paint(_cx: number, _cy: number, _canvas: ICanvas) {} + public paint(_cx: number, _cy: number, _canvas: ICanvas) { } // public height: number = 30; // public paint(cx: number, cy: number, canvas: ICanvas) { @@ -260,25 +339,37 @@ export class BarLayoutingInfo { } public spaceToForce(space: number): number { - if(this._timeSortedSprings.length > 0) { - space -= this._timeSortedSprings[0].preSpringWidth + if (this.totalSpringConstant !== -1) { + if (this._timeSortedSprings.length > 0) { + space -= this._timeSortedSprings[0].preSpringWidth + } + space -= this._incompleteGraceRodsWidth; + return Math.max(space, 0) * this.totalSpringConstant; } - return space * this.totalSpringConstant; + return -1; } public calculateVoiceWidth(force: number): number { - let width = this.calculateWidth(force, this.totalSpringConstant); - if(this._timeSortedSprings.length > 0) { - width += this._timeSortedSprings[0].preSpringWidth + let width = 0; + if (this.totalSpringConstant !== -1) { + width = this.calculateWidth(force, this.totalSpringConstant); + } + + if (this._timeSortedSprings.length > 0) { + width += this._timeSortedSprings[0].preSpringWidth; } + width += this._incompleteGraceRodsWidth; return width; } - public calculateWidth(force: number, springConstant: number): number { + private calculateWidth(force: number, springConstant: number): number { return force / springConstant; } public buildOnTimePositions(force: number): Map { + if(this.totalSpringConstant === -1) { + return new Map(); + } if (ModelUtils.isAlmostEqualTo(this._onTimePositionsForce, force) && this._onTimePositions) { return this._onTimePositions; } diff --git a/src/rendering/staves/Spring.ts b/src/rendering/staves/Spring.ts index 31a8cb57e..0268b2618 100644 --- a/src/rendering/staves/Spring.ts +++ b/src/rendering/staves/Spring.ts @@ -9,7 +9,13 @@ export class Spring { return this.preSpringWidth + this.postSpringWidth; } - public preSpringWidth: number = 0; + public preBeatWidth: number = 0; + public graceBeatWidth: number = 0; + public postSpringWidth: number = 0; - public allDurations: number[] = []; + public get preSpringWidth() { + return this.preBeatWidth + this.graceBeatWidth; + } + + public allDurations: Set = new Set(); } diff --git a/test-data/visual-tests/notation-legend/bends-default.png b/test-data/visual-tests/notation-legend/bends-default.png index 04b4a6ec4..ca71d1481 100644 Binary files a/test-data/visual-tests/notation-legend/bends-default.png and b/test-data/visual-tests/notation-legend/bends-default.png differ diff --git a/test-data/visual-tests/notation-legend/bends-songbook.png b/test-data/visual-tests/notation-legend/bends-songbook.png index ca187f54d..14d1b7986 100644 Binary files a/test-data/visual-tests/notation-legend/bends-songbook.png and b/test-data/visual-tests/notation-legend/bends-songbook.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 b3f309d8f..3f687e32c 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 95b9be251..caa57694f 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/grace-default.png b/test-data/visual-tests/notation-legend/grace-default.png index c233ec4e5..ab235629c 100644 Binary files a/test-data/visual-tests/notation-legend/grace-default.png and b/test-data/visual-tests/notation-legend/grace-default.png differ diff --git a/test-data/visual-tests/notation-legend/grace-songbook.png b/test-data/visual-tests/notation-legend/grace-songbook.png index 36393c4f4..58469777c 100644 Binary files a/test-data/visual-tests/notation-legend/grace-songbook.png and b/test-data/visual-tests/notation-legend/grace-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 f46c9fee8..f6a5fadd9 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 99eeac65a..392e62153 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/multi-grace-default.png b/test-data/visual-tests/notation-legend/multi-grace-default.png index b9f4813de..22ddc0213 100644 Binary files a/test-data/visual-tests/notation-legend/multi-grace-default.png and b/test-data/visual-tests/notation-legend/multi-grace-default.png differ diff --git a/test-data/visual-tests/notation-legend/multi-grace-songbook.png b/test-data/visual-tests/notation-legend/multi-grace-songbook.png index 57954e928..5b58bd1ac 100644 Binary files a/test-data/visual-tests/notation-legend/multi-grace-songbook.png and b/test-data/visual-tests/notation-legend/multi-grace-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png b/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png index 9ddc3cc10..380d71a35 100644 Binary files a/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png and b/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png differ diff --git a/test-data/visual-tests/special-notes/grace-notes-advanced.gp b/test-data/visual-tests/special-notes/grace-notes-advanced.gp index f187bec76..e6d0c06a0 100644 Binary files a/test-data/visual-tests/special-notes/grace-notes-advanced.gp and b/test-data/visual-tests/special-notes/grace-notes-advanced.gp differ diff --git a/test-data/visual-tests/special-notes/grace-notes-advanced.png b/test-data/visual-tests/special-notes/grace-notes-advanced.png index 2f485d8ed..092cf103f 100644 Binary files a/test-data/visual-tests/special-notes/grace-notes-advanced.png and b/test-data/visual-tests/special-notes/grace-notes-advanced.png differ diff --git a/test-data/visual-tests/special-notes/grace-notes.png b/test-data/visual-tests/special-notes/grace-notes.png index b1c640ad6..90e280aa4 100644 Binary files a/test-data/visual-tests/special-notes/grace-notes.png and b/test-data/visual-tests/special-notes/grace-notes.png differ