365 lines
12 KiB
C#
365 lines
12 KiB
C#
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 {
|
|
/// <summary> All the notes contain the same pitch need to render a straight bar.</summary>
|
|
Basic,
|
|
|
|
/// <summary> All Notes are either increasing or decreasing in pitch.</summary>
|
|
Angle,
|
|
|
|
/// <summary> Notes are connected by a straight beam.</summary>
|
|
Straight
|
|
}
|
|
|
|
public List<ABC.Duration> items { get; } = new List<ABC.Duration>();
|
|
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <summary> Analyzes the items in the beam and determines relevant info that will control layout. </summary>
|
|
public void Analyze()
|
|
{
|
|
DetermineDirection();
|
|
|
|
if (IsBasic())
|
|
type = Type.Basic;
|
|
else if (IsAngled())
|
|
type = Type.Angle;
|
|
else
|
|
{
|
|
type = Type.Straight;
|
|
DetermineStemHeight();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines the stem height to use for a straight beam.
|
|
/// The Height is calculated as the default terminus of the highest note's stem
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public bool CreateAngledBeam(List<Vector3> 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<Vector3> vertices, Material material, GameObject container)
|
|
{
|
|
var mesh = new Mesh();
|
|
mesh.vertices = vertices.ToArray();
|
|
|
|
var triangles = new List<int>();
|
|
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>();
|
|
meshRenderer.sharedMaterial = material;
|
|
|
|
var meshFilter = item.AddComponent<MeshFilter>();
|
|
meshFilter.mesh = mesh;
|
|
|
|
item.transform.localPosition = Vector3.zero;
|
|
item.transform.parent = container.transform;
|
|
}
|
|
|
|
public static Dictionary<int, Beam> CreateBeams(ABC.Tune tune)
|
|
{
|
|
var beams = new Dictionary<int, Beam>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
} |