using UnityEngine; using System.Collections.Generic; namespace ABCUnity { class Beam { public const float unspecifiedStemHeight = float.MaxValue; public const float defaultStemHeight = 1.92f; public static Vector3 stemUpOffset = new Vector3(0.65f, 0.35f, 0.0f); public static Vector3 stemDownOffset = new Vector3(0.0f, 0.262f, 0.0f); const float beamHeight = 0.28f; static Vector3 beamOffset = new Vector3(0.0f, 0.366f, 0.0f); const float defaultBeamSpacer = 0.2f; public NoteCreator.NoteDirection noteDirection { get; private set; } public enum Type { /// All the notes contain the same pitch need to render a straight bar. Basic, /// All Notes are either increasing or decreasing in pitch. Angle, /// Notes are connected by a straight beam. Straight } public List items { get; } = new List(); public Type type { get; private set; } int id { get; } public ABC.Clef clef { get; } public float stemHeight { get; set; } = unspecifiedStemHeight; private int index = 0; public bool isReadyToCreate { get { return index == items.Count; } } public Beam(int id, ABC.Clef clef) { this.id = id; this.clef = clef; } private ABC.Pitch GetPitchForItem(ABC.Item item) { switch(item.type) { case ABC.Item.Type.Note: var note = item as ABC.Note; return note.pitch; case ABC.Item.Type.Chord: var chord = item as ABC.Chord; if (noteDirection == NoteCreator.NoteDirection.Down) return chord.notes[0].pitch; else return chord.notes[chord.notes.Length - 1].pitch; default: throw new LayoutException($"Invalid item in beam: {id}"); } } /// Analyzes the items in the beam and determines relevant info that will control layout. public void Analyze() { DetermineDirection(); if (IsBasic()) type = Type.Basic; else if (IsAngled()) type = Type.Angle; else { type = Type.Straight; DetermineStemHeight(); } } /// /// Determines the stem height to use for a straight beam. /// The Height is calculated as the default terminus of the highest note's stem /// void DetermineStemHeight() { if (noteDirection == NoteCreator.NoteDirection.Up) { ABC.Pitch pitch = ABC.Pitch.A0; foreach (var item in items) { var itemPitch = GetPitchForItem(item); if (itemPitch > pitch) pitch = itemPitch; } int stepCount = pitch - NoteCreator.clefZero[clef]; stemHeight = NoteCreator.noteStep * stepCount + defaultStemHeight; } else { ABC.Pitch pitch = ABC.Pitch.C8; foreach (var item in items) { var itemPitch = GetPitchForItem(item); if (itemPitch < pitch) pitch = itemPitch; } int stepCount = pitch - NoteCreator.clefZero[clef]; stemHeight = NoteCreator.noteStep * (stepCount + 1) - defaultStemHeight; } } /// /// Determines the direction of the beam stem. /// If the average of all notes is above the middle staff value they will be down else, up. /// private void DetermineDirection() { int total = 0; foreach (var item in items) { switch (item.type) { case ABC.Item.Type.Note: var note = item as ABC.Note; total += (int)note.pitch; break; case ABC.Item.Type.Chord: var chord = item as ABC.Chord; int sum = 0; foreach (var chordNote in chord.notes) sum += (int)chordNote.pitch; total += (int)Mathf.Round(sum / (float)chord.notes.Length); break; default: throw new LayoutException($"Invalid item in beam: {id}"); } } float averagePitch = total / (float)items.Count; noteDirection = (averagePitch > (float)NoteCreator.clefZero[clef] + 3) ? NoteCreator.NoteDirection.Down : NoteCreator.NoteDirection.Up; } Bounds first; Bounds last; /// /// This method is called every time an item in the beam has been laid out. /// When the final item is laid out the beam will be added to the scene. /// public void Update(Bounds bounds) { if (index == 0) first = bounds; if (index < items.Count) { last = bounds; index += 1; } } bool IsBasic() { var firstNotePitch = GetPitchForItem(items[0]); for (int i = 0; i < items.Count; i++) { if (GetPitchForItem(items[i]) != firstNotePitch) return false; } return true; } bool IsAngled() { var previousNote = GetPitchForItem(items[0]); var currentNote = GetPitchForItem(items[1]); if (currentNote > previousNote) { previousNote = currentNote; for (int i = 2; i < items.Count; i++) { currentNote = GetPitchForItem(items[i]); if (currentNote <= previousNote) return false; previousNote = currentNote; } return true; } else if (currentNote < previousNote) { previousNote = currentNote; for (int i = 2; i < items.Count; i++) { currentNote = GetPitchForItem(items[i]); if (currentNote >= previousNote) return false; previousNote = currentNote; } return true; } return false; } /// /// Creates a basic straight beam connecting the first and last item in this beam. /// Note that this method should be used when the first and last items have the same max Y bounding value. /// public bool CreateBasicBeam(SpriteCache cache, GameObject container) { if (type != Type.Basic && type != Type.Straight) return false; float offsetY = 0.0f; ABC.Length length = (items[0] as ABC.Duration).length; for (ABC.Length l = ABC.Length.Eighth; l >= length; l--) { var beam = cache.GetSpriteObject($"Note_Beam_{noteDirection}"); beam.transform.parent = container.transform; float distX; if (noteDirection == NoteCreator.NoteDirection.Up) { var beamPos = first.max; beam.transform.localPosition = new Vector3(beamPos.x, beamPos.y - offsetY, 0.0f); distX = last.max.x - beamPos.x; } else { var beamPos = first.min; beam.transform.localPosition = new Vector3(beamPos.x, beamPos.y + offsetY, 0.0f); distX = last.min.x - beamPos.x; } beam.transform.localScale = new Vector3(distX, 1.0f, 1.0f); offsetY += beamHeight + defaultBeamSpacer; } return true; } /// /// Creates a beam connecting the first and last item in this beam. /// This method should be used when the max values for these items are not at the same height. /// Mesh vertices are generated as opposed to a sprite as in the straight beam. /// public bool CreateAngledBeam(List vertices) { if (type != Type.Angle) return false; float offsetY = 0.0f; ABC.Length length = (items[0] as ABC.Duration).length; for (ABC.Length l = ABC.Length.Eighth; l >= length; l--) { if (noteDirection == NoteCreator.NoteDirection.Up) { vertices.Add(new Vector3(first.max.x, first.max.y - offsetY, 0.0f)); vertices.Add(new Vector3(last.max.x, last.max.y - offsetY, 0.0f)); vertices.Add(new Vector3(first.max.x, first.max.y - offsetY - beamHeight, 0.0f)); vertices.Add(new Vector3(last.max.x, last.max.y - offsetY - beamHeight, 0.0f)); } else { vertices.Add(new Vector3(first.min.x, first.min.y + offsetY + beamHeight, 0.0f)); vertices.Add(new Vector3(last.min.x, last.min.y + offsetY + beamHeight, 0.0f)); vertices.Add(new Vector3(first.min.x, first.min.y + offsetY, 0.0f)); vertices.Add(new Vector3(last.min.x, last.min.y + offsetY, 0.0f)); } offsetY += beamHeight + defaultBeamSpacer; } return true; } public static void CreateMesh(List vertices, Material material, GameObject container) { var mesh = new Mesh(); mesh.vertices = vertices.ToArray(); var triangles = new List(); for (int i = 0; i < vertices.Count; i += 4) { triangles.Add(i); triangles.Add(i + 1); triangles.Add(i + 2); triangles.Add(i + 2); triangles.Add(i + 1); triangles.Add(i + 3); } mesh.triangles = triangles.ToArray(); var item = new GameObject("Beam"); var meshRenderer = item.AddComponent(); meshRenderer.sharedMaterial = material; var meshFilter = item.AddComponent(); meshFilter.mesh = mesh; item.transform.localPosition = Vector3.zero; item.transform.parent = container.transform; } public static Dictionary CreateBeams(ABC.Tune tune) { var beams = new Dictionary(); foreach (var voice in tune.voices) { foreach (var item in voice.items) { var duration = item as ABC.Duration; if (duration == null) continue; // add this note to a beam if necessary if (duration.beam != 0) { beams.TryGetValue(duration.beam, out Beam beam); if (beam == null) { beam = new Beam(duration.beam, voice.clef); beams[duration.beam] = beam; } beam.items.Add(duration); } } } foreach (var item in beams) item.Value.Analyze(); return beams; } } }