Skip to content

Commit 2d7f562

Browse files
committed
Render ties and slurs which appear on the same score line
1 parent 026aad1 commit 2d7f562

File tree

8 files changed

+409
-21
lines changed

8 files changed

+409
-21
lines changed

Runtime/Scripts/Alignment.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using ABC;
34
using UnityEngine;
45

56
namespace ABCUnity
@@ -49,8 +50,11 @@ private bool IsMeasureRest()
4950
public List<Measure> measures { get; private set; }
5051
TimeSignature timeSignature;
5152

53+
public Voice voice {get; private set;}
54+
5255
public void Create(ABC.Voice voice)
5356
{
57+
this.voice = voice;
5458
measures = new List<Measure>();
5559

5660
if (voice.items.Count == 0) return;

Runtime/Scripts/Grouping.cs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
using UnityEngine;
2+
using System.Collections.Generic;
3+
using UnityEditor.Experimental.GraphView;
4+
using System.Runtime.InteropServices.WindowsRuntime;
5+
using System;
6+
using UnityEditor;
7+
8+
namespace ABCUnity
9+
{
10+
static class Grouping
11+
{
12+
const float ParaoblaMidpointScale = 0.3f;
13+
14+
private enum SlurPosition {Above, Below};
15+
16+
public static LineRenderer Create(VoiceLayout.ScoreLine.Element startElement, VoiceLayout.ScoreLine.Element endElement, Material material)
17+
{
18+
var elements = CollectElements(startElement, endElement);
19+
return CreateSingleScorelineSlur(elements, material);
20+
}
21+
22+
private static LineRenderer CreateSingleScorelineSlur(List<VoiceLayout.ScoreLine.Element> elements, Material material)
23+
{
24+
var startElement = elements[0];
25+
var endElement = elements[elements.Count - 1];
26+
var scoreLine = startElement.measure.scoreLine;
27+
28+
var slurPosition = DetermineSlurPosition(elements);
29+
var startPos = startElement.container.transform.localPosition + startElement.measure.container.transform.localPosition;
30+
Vector3 startAnchor, endAnchor;
31+
32+
var endPos = endElement.container.transform.localPosition + endElement.measure.container.transform.localPosition;
33+
34+
if (slurPosition == SlurPosition.Below)
35+
{
36+
startPos += new Vector3(startElement.info.rootBounding.max.x, startElement.info.rootBounding.min.y, 0.0f);
37+
startAnchor = startPos;
38+
39+
startPos += new Vector3(0.1f, -0.1f, 0.0f);
40+
startAnchor.x -= startElement.info.rootBounding.extents.x;
41+
42+
endPos += endElement.info.rootBounding.min;
43+
endAnchor = endPos;
44+
45+
endPos += new Vector3(-0.1f, -0.1f, 0.0f);
46+
endAnchor.x += endElement.info.rootBounding.extents.x;
47+
}
48+
else
49+
{
50+
startPos += new Vector3(startElement.info.rootBounding.max.x, startElement.info.rootBounding.max.y, 0.0f);
51+
startAnchor = startPos;
52+
53+
startPos += new Vector3(0.1f, 0.1f, 0.0f);
54+
startAnchor.x -= startElement.info.rootBounding.extents.x;
55+
56+
endPos += new Vector3(endElement.info.rootBounding.min.x, endElement.info.rootBounding.max.y, 0.0f);
57+
endAnchor = endPos;
58+
59+
endPos += new Vector3(-0.1f, 0.1f, 0.0f);
60+
endAnchor.x += endElement.info.rootBounding.extents.x;
61+
}
62+
63+
var boundingY = GetSlurBoundingY(elements, slurPosition);
64+
Vector3 boundingPt1 = new Vector3(0.0f, boundingY, 0.0f);
65+
Vector3 boundingPt2 = new Vector3(1.0f, boundingY, 0.0f);
66+
67+
var lineMidpoint = (startPos + endPos) / 2.0f;
68+
var anchorMidpoint = (startAnchor + endAnchor) / 2.0f;
69+
70+
71+
var slurMidpoint = MathUtil.LineIntersect(lineMidpoint, anchorMidpoint, boundingPt1, boundingPt2);
72+
var direction = (lineMidpoint - anchorMidpoint).normalized * ParaoblaMidpointScale;
73+
74+
var slurLinePoints = CreatePoints(startPos, slurMidpoint + direction, endPos);
75+
76+
return CreateLineRenderer(scoreLine, slurLinePoints, material);
77+
}
78+
79+
static List<Vector3> CreatePoints(Vector3 startPos, Vector3 midpoint, Vector3 endPos)
80+
{
81+
var slurPositions = new List<Vector3>();
82+
83+
slurPositions.Add(startPos);
84+
85+
float[,] matrix = new float[3, 4]{
86+
{ startPos.x * startPos.x, startPos.x, 1, startPos.y },
87+
{ midpoint.x * midpoint.x, midpoint.x, 1, midpoint.y },
88+
{ endPos.x * endPos.x, endPos.x, 1, endPos.y }
89+
};
90+
91+
matrix = MathUtil.ReducedRowEchelonForm(matrix);
92+
float a = matrix[0, 3];
93+
float b = matrix[1, 3];
94+
float c = matrix[2, 3];
95+
96+
const int segmentCount = 20;
97+
float step = (endPos.x - startPos.x) / segmentCount;
98+
float x = startPos.x;
99+
for (int i = 0; i < segmentCount; i++) {
100+
x += step;
101+
102+
float y = a * (x*x) + b * x + c;
103+
104+
slurPositions.Add(new Vector3(x, y, 0));
105+
}
106+
107+
return slurPositions;
108+
}
109+
110+
private static LineRenderer CreateLineRenderer(VoiceLayout.ScoreLine scoreLine, List<Vector3> slurPositions, Material material)
111+
{
112+
if (scoreLine.slurs == null) {
113+
scoreLine.slurs = new GameObject("Slurs");
114+
scoreLine.slurs.transform.SetParent(scoreLine.container.transform, false);
115+
}
116+
117+
var slur = new GameObject("Slur");
118+
slur.transform.SetParent(scoreLine.slurs.transform, false);
119+
120+
var lineRenderer = slur.AddComponent<LineRenderer>();
121+
lineRenderer.positionCount = slurPositions.Count;
122+
lineRenderer.SetPositions(slurPositions.ToArray());
123+
124+
lineRenderer.useWorldSpace = false;
125+
lineRenderer.startWidth = 0.1f;
126+
lineRenderer.endWidth = 0.1f;
127+
128+
lineRenderer.material = material;
129+
130+
return lineRenderer;
131+
}
132+
133+
/// <summary>
134+
/// Returns a list containing all the elements between start and end elements inclusive.
135+
/// </summary>
136+
137+
private static List<VoiceLayout.ScoreLine.Element> CollectElements(VoiceLayout.ScoreLine.Element startElement, VoiceLayout.ScoreLine.Element endElement)
138+
{
139+
List<VoiceLayout.ScoreLine.Element> elements = new List<VoiceLayout.ScoreLine.Element>();
140+
var currentElement = startElement;
141+
var elementIndex = currentElement.measure.elements.FindIndex(e => e == startElement);
142+
143+
var currentMeasure = currentElement.measure;
144+
var measureIndex = currentMeasure.scoreLine.measures.FindIndex(m => m == currentMeasure);
145+
146+
var currentScoreLine = currentMeasure.scoreLine;
147+
var scoreLineIndex = currentScoreLine.voiceLayout.scoreLines.FindIndex(sl => sl == currentScoreLine);
148+
149+
var voiceLayout = currentScoreLine.voiceLayout;
150+
151+
while (currentMeasure.elements[elementIndex] != endElement) {
152+
elements.Add(currentMeasure.elements[elementIndex++]);
153+
154+
if (elementIndex >= currentMeasure.elements.Count)
155+
{
156+
elementIndex = 0;
157+
measureIndex += 1;
158+
}
159+
160+
if (measureIndex >= currentScoreLine.measures.Count) {
161+
measureIndex = 0;
162+
scoreLineIndex += 1;
163+
}
164+
165+
currentScoreLine = voiceLayout.scoreLines[scoreLineIndex];
166+
currentMeasure = currentScoreLine.measures[measureIndex];
167+
currentElement = currentMeasure.elements[elementIndex];
168+
}
169+
170+
elements.Add(endElement);
171+
172+
return elements;
173+
}
174+
175+
/// Determines the Position of the slur by looking at note directions.
176+
/// If the stems point up then the slur will be placed below.
177+
/// If the stems point down then the slur will be placed above.
178+
/// If the stems are mixed then the slur will be placed above
179+
private static SlurPosition DetermineSlurPosition(List<VoiceLayout.ScoreLine.Element> elements)
180+
{
181+
182+
var clef = elements[0].measure.scoreLine.voiceLayout.voice.clef;
183+
184+
// get the initial direction
185+
int i = 0;
186+
var initialDirection = NoteCreator.NoteDirection.Unknown;
187+
188+
for (; i < elements.Count; i++)
189+
{
190+
if (initialDirection != NoteCreator.NoteDirection.Unknown)
191+
break;
192+
193+
initialDirection = NoteCreator.DetermineNoteDirection(elements[i].item, clef);
194+
}
195+
196+
for (; i < elements.Count; i++)
197+
{
198+
var direction = NoteCreator.DetermineNoteDirection(elements[i].item, clef);
199+
if (direction == NoteCreator.NoteDirection.Unknown)
200+
continue;
201+
202+
if (direction != initialDirection)
203+
return SlurPosition.Above;
204+
}
205+
206+
return initialDirection == NoteCreator.NoteDirection.Up ? SlurPosition.Below : SlurPosition.Above;
207+
}
208+
209+
private static float GetSlurBoundingY(List<VoiceLayout.ScoreLine.Element> elements, SlurPosition slurPosition)
210+
{
211+
if (slurPosition == SlurPosition.Above)
212+
{
213+
float max = float.MinValue;
214+
foreach(var element in elements)
215+
{
216+
max = Math.Max(max, element.info.rootBounding.max.y);
217+
}
218+
219+
return max;
220+
}
221+
else
222+
{
223+
float min = float.MaxValue;
224+
foreach(var element in elements)
225+
{
226+
min = Math.Min(min, element.info.rootBounding.min.y);
227+
}
228+
229+
return min;
230+
}
231+
}
232+
}
233+
}

Runtime/Scripts/Grouping.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/Scripts/Layout.cs

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class Layout : MonoBehaviour
1212
[SerializeField] private SpriteAtlas spriteAtlas; // set in editor
1313
[SerializeField] public Color color = Color.black;
1414
[SerializeField] public Material NoteMaterial;
15+
[SerializeField] public Material LineMaterial;
1516
[SerializeField] public TextMeshPro textPrefab;
1617
[SerializeField] public float staffLinePadding = 0.4f;
1718
[SerializeField] public float staffLineMargin = 1.0f;
@@ -28,7 +29,7 @@ public class Layout : MonoBehaviour
2829
public ABC.Tune tune { get; private set; }
2930
GameObject scoreContainer;
3031
public Dictionary<int, GameObject> gameObjectMap { get; } = new Dictionary<int, GameObject>();
31-
public Dictionary<GameObject, ABC.Item> itemMap { get; } = new Dictionary<GameObject, ABC.Item>();
32+
private Dictionary<int, VoiceLayout.ScoreLine.Element> abcItemToLayoutElement { get; } = new Dictionary<int, VoiceLayout.ScoreLine.Element>();
3233
private Dictionary<int, List<SpriteRenderer>> spriteRendererCache = new Dictionary<int, List<SpriteRenderer>>();
3334
private TimeSignature timeSignature;
3435
#endregion
@@ -69,7 +70,7 @@ public void Clear()
6970
GameObject.Destroy(scoreContainer);
7071
layouts.Clear();
7172
gameObjectMap.Clear();
72-
itemMap.Clear();
73+
abcItemToLayoutElement.Clear();
7374
spriteRendererCache.Clear();
7475

7576
timeSignature = null;
@@ -114,14 +115,6 @@ public void LoadFile(string path)
114115
}
115116
}
116117

117-
public GameObject FindItemRootObject(GameObject obj)
118-
{
119-
while (!itemMap.ContainsKey(obj))
120-
obj = obj.transform.parent.gameObject;
121-
122-
return obj;
123-
}
124-
125118
public bool SetItemColor(ABC.Item item, Color color)
126119
{
127120
if (gameObjectMap.TryGetValue(item.id, out GameObject obj))
@@ -236,7 +229,7 @@ void LayoutScoreLine(int lineNum)
236229
}
237230

238231
gameObjectMap.Add(element.item.id, element.container);
239-
itemMap.Add(element.container, element.item);
232+
abcItemToLayoutElement.Add(element.item.id, element);
240233

241234
// position
242235
var beatItem = layoutMeasure.elements[layoutMeasure.elements.Count - 1];
@@ -537,9 +530,29 @@ void LayoutTune()
537530
for (int i = 0; i < layouts[0].scoreLines.Count; i++)
538531
PositionScoreLine(i);
539532

533+
CreateSlursAndTies();
534+
540535
scoreContainer.transform.localScale = new Vector3(layoutScale, layoutScale, layoutScale);
541536
this.gameObject.transform.localScale = scale;
542-
537+
}
538+
539+
private void CreateSlursAndTies()
540+
{
541+
foreach (var voice in tune.voices)
542+
{
543+
CreateGroupings(voice.slurs);
544+
CreateGroupings(voice.ties);
545+
}
546+
}
547+
548+
private void CreateGroupings(IEnumerable<ABC.Grouping> groupings)
549+
{
550+
foreach (var grouping in groupings)
551+
{
552+
var startElement = abcItemToLayoutElement[grouping.startId];
553+
var endElement = abcItemToLayoutElement[grouping.endId];
554+
Grouping.Create(startElement, endElement, LineMaterial);
555+
}
543556
}
544557

545558
/// <summary> Breaks up a single score line into multiple lines based on the horizontal max</summary>
@@ -552,7 +565,7 @@ void PartitionScoreLine()
552565
{
553566
scoreLines[i] = layouts[i].scoreLines[0].measures;
554567
layouts[i].scoreLines.Clear();
555-
layouts[i].scoreLines.Add(new VoiceLayout.ScoreLine());
568+
layouts[i].scoreLines.Add(new VoiceLayout.ScoreLine(layouts[i]));
556569
}
557570

558571
float currentWidth = 0.0f;
@@ -570,7 +583,7 @@ void PartitionScoreLine()
570583
if (currentWidth + scoreLine[measureIndex].insertX > horizontalMax)
571584
{
572585
foreach (var layout in layouts)
573-
layout.scoreLines.Add(new VoiceLayout.ScoreLine());
586+
layout.scoreLines.Add(new VoiceLayout.ScoreLine(layout));
574587

575588
currentWidth = 0.0f;
576589
break;
@@ -581,7 +594,7 @@ void PartitionScoreLine()
581594
for (int i = 0; i < scoreLines.Length; i++)
582595
{
583596
var scoreLine = layouts[i].scoreLines[layouts[i].scoreLines.Count - 1];
584-
scoreLine.measures.Add(scoreLines[i][measureIndex]);
597+
scoreLine.AddMeasure(scoreLines[i][measureIndex]);
585598
currentWidth += measureWidth;
586599
}
587600
}

0 commit comments

Comments
 (0)