@@ -0,0 +1,263 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fractions import Fraction
|
||||
|
||||
from generator.model import beat_lengths, fraction
|
||||
from generator.engraving.fonts import Fonts
|
||||
from generator.engraving.notation import ScoreIndex, flags
|
||||
from generator.engraving.types import Anchor, Primitive, Style, bounds, curve, line, polygon
|
||||
|
||||
def beam_groups(index: ScoreIndex) -> tuple[tuple[str, ...], ...]:
|
||||
if index._beam_groups is not None:
|
||||
return index._beam_groups
|
||||
groups = []
|
||||
for voice in dict.fromkeys(e.voice for e in index.events):
|
||||
current = []
|
||||
previous_key = None
|
||||
for event in (e for e in index.events if e.voice == voice):
|
||||
note = event.value
|
||||
primary = next((b['type'] for b in note.get('beams', []) if b['level'] == 1), None)
|
||||
measure_start = next(start for start, _, measure in index.measures if measure['id'] == event.measure['id'])
|
||||
beats = beat_lengths(index.state(event.start)['meter'])
|
||||
offset = event.start - measure_start
|
||||
beat = 0
|
||||
edge = Fraction()
|
||||
for duration in beats:
|
||||
edge += duration
|
||||
if offset < edge:
|
||||
break
|
||||
beat += 1
|
||||
key = (event.measure['id'], beat, tuple(note.get('tuplet_ids', [])), bool(note.get('grace')))
|
||||
eligible = note['kind'] == 'note' and flags(note['written']) and note.get('stem') != 'none' and not note.get('hidden')
|
||||
if not eligible or primary == 'begin' or (primary is None and key != previous_key):
|
||||
if current:
|
||||
groups.append(current)
|
||||
current = []
|
||||
if eligible:
|
||||
current.append(note['id'])
|
||||
if primary == 'end':
|
||||
groups.append(current)
|
||||
current = []
|
||||
previous_key = key
|
||||
if current:
|
||||
groups.append(current)
|
||||
index._beam_groups = tuple(tuple(g) for g in groups if len(g) > 1 or index.by_id[g[0]].value.get('beams'))
|
||||
return index._beam_groups
|
||||
|
||||
def draw_beams(index: ScoreIndex, anchors: list[Anchor], items: list[Primitive], start: Fraction, end: Fraction, left: float, right: float, style: Style) -> list[Primitive]:
|
||||
additions = []
|
||||
removed = set()
|
||||
for group in beam_groups(index):
|
||||
selected = []
|
||||
for identifier in group:
|
||||
candidates = [a for a in anchors if a.event == identifier and a.flags]
|
||||
if candidates:
|
||||
selected.append(candidates[0])
|
||||
if not selected:
|
||||
continue
|
||||
direction = selected[0].direction
|
||||
if any(a.direction != direction for a in selected):
|
||||
continue
|
||||
sources = tuple(a.event for a in selected)
|
||||
removed.update(sources)
|
||||
beam_y = min(a.stem_y for a in selected) if direction == -1 else max(a.stem_y for a in selected)
|
||||
first, last = selected[0], selected[-1]
|
||||
x1 = left if index.by_id[group[0]].start < start else first.stem_x
|
||||
x2 = right if index.by_id[group[-1]].start >= end else last.stem_x
|
||||
for anchor in selected:
|
||||
additions.append(line('beam-stem', (anchor.event,), ((anchor.stem_x, anchor.stem_y), (anchor.stem_x, beam_y)), 1.4))
|
||||
for level in range(1, max(a.flags for a in selected) + 1):
|
||||
y = beam_y - direction * (level - 1) * style.space * 0.75
|
||||
for i, anchor in enumerate(selected):
|
||||
if anchor.flags < level:
|
||||
continue
|
||||
event = index.by_id[anchor.event].value
|
||||
explicit = next((b['type'] for b in event.get('beams', []) if b['level'] == level), None)
|
||||
next_anchor = selected[i + 1] if i + 1 < len(selected) else None
|
||||
previous = selected[i - 1] if i else None
|
||||
a = anchor.stem_x
|
||||
if explicit in ('forward hook', 'backward hook'):
|
||||
b = a + (8 if explicit == 'forward hook' else -8)
|
||||
elif next_anchor and next_anchor.flags >= level and explicit != 'end':
|
||||
b = next_anchor.stem_x
|
||||
elif i == len(selected) - 1 and x2 > a and explicit != 'end':
|
||||
b = x2
|
||||
elif i == 0 and x1 < a and explicit != 'begin':
|
||||
b = x1
|
||||
elif not previous or previous.flags < level or explicit == 'begin':
|
||||
b = a + (8 if next_anchor else -8)
|
||||
else:
|
||||
continue
|
||||
additions.append(polygon('beam', sources, ((a, y), (b, y), (b, y - direction * style.space * 0.5), (a, y - direction * style.space * 0.5))))
|
||||
if selected and x1 < selected[0].stem_x and selected[0].flags >= level:
|
||||
additions.append(line('beam', sources, ((x1, y), (selected[0].stem_x, y)), style.space * 0.5))
|
||||
return [p for p in items if not (p.role == 'flag' and any(s in removed for s in p.sources))] + additions
|
||||
|
||||
def draw_spans(index: ScoreIndex, anchors: list[Anchor], items: list[Primitive], start: Fraction, end: Fraction, left: float, right: float, fonts: Fonts, style: Style) -> list[Primitive]:
|
||||
result = []
|
||||
by_tone = {a.source: a for a in anchors}
|
||||
by_event = {a.event: a for a in anchors}
|
||||
for identifier, (event, tone) in index.tones.items():
|
||||
target = tone.get('tie_to')
|
||||
tone_end = event.start + fraction(tone['duration'])
|
||||
anchor = by_tone.get(identifier)
|
||||
if anchor and anchor.fragment.end < tone_end:
|
||||
target = identifier
|
||||
if target:
|
||||
target_event = index.tones[target][0]
|
||||
target_time = tone_end if target == identifier else target_event.start
|
||||
if event.start >= end or target_time < start:
|
||||
continue
|
||||
destination = by_tone.get(target) if target != identifier else None
|
||||
if anchor is None and destination is None:
|
||||
continue
|
||||
reference = anchor or destination
|
||||
sign = -reference.direction
|
||||
x1 = anchor.x + style.space * 1.4 if anchor else left
|
||||
x2 = destination.x - 2 if destination else right
|
||||
y1 = anchor.y + sign * 8 if anchor else reference.y + sign * 8
|
||||
y2 = destination.y + sign * 8 if destination else y1
|
||||
if x2 > x1:
|
||||
result.append(curve('tie', (identifier, target), ((x1, y1), (x1 + (x2 - x1) / 3, y1 + sign * 9), (x2 - (x2 - x1) / 3, y2 + sign * 9), (x2, y2)), 1.6))
|
||||
if anchor and anchor.fragment.start > event.start:
|
||||
y = anchor.y - anchor.direction * 8
|
||||
if anchor.x - 2 > left:
|
||||
result.append(curve('tie', (identifier,), ((left, y), (left + 5, y - anchor.direction * 9), (anchor.x - 7, y - anchor.direction * 9), (anchor.x - 2, y)), 1.6))
|
||||
top = min(-style.space * 5, min((p.box.top for p in items if p.role not in ('harmony', 'lyric', 'section', 'tempo')), default=0))
|
||||
tuplet_lanes = []
|
||||
for span in index.staff.get('tuplets', []):
|
||||
a, b = index.by_id[span['start']], index.by_id[span['end']]
|
||||
if b.end <= start or a.start >= end:
|
||||
continue
|
||||
members = [anchor for anchor in anchors if span['id'] in index.by_id[anchor.event].value.get('tuplet_ids', [])]
|
||||
if not members:
|
||||
continue
|
||||
x1 = left if a.start < start else min(m.stem_x for m in members) - 3
|
||||
x2 = right if b.end > end else max(m.stem_x for m in members) + style.space
|
||||
y = top - 16 - span.get('depth', 0) * 23
|
||||
while any(x1 < b and a < x2 and abs(y - lane) < 22 for a, b, lane in tuplet_lanes):
|
||||
y -= 23
|
||||
tuplet_lanes.append((x1, x2, y))
|
||||
number = span.get('show_number', 'actual')
|
||||
label = '' if number == 'none' else str(span['note_count'])
|
||||
if number == 'both':
|
||||
label += ':' + str(span['space_count'])
|
||||
if span.get('show_type', 'none') != 'none':
|
||||
actual = span.get('tuplet_actual', {}).get('type', a.value['written']['type'])
|
||||
normal = span.get('tuplet_normal', {}).get('type', actual)
|
||||
label += ' ' + actual + '.' * span.get('tuplet_actual', {}).get('dots', 0)
|
||||
if span['show_type'] == 'both':
|
||||
label += ' : ' + normal + '.' * span.get('tuplet_normal', {}).get('dots', 0)
|
||||
references = (span['id'],)
|
||||
text = fonts.text(label, (x1 + x2) / 2, y + 4, 15, 'tuplet-number', references, True) if label else None
|
||||
if text:
|
||||
result.append(text)
|
||||
if span.get('bracket', 'auto') != 'no':
|
||||
intervals = [(x1, text.box.left - 4), (text.box.right + 4, x2)] if text else [(x1, x2)]
|
||||
for p, q in intervals:
|
||||
if q <= p:
|
||||
continue
|
||||
if span.get('line_shape') == 'curved':
|
||||
result.append(curve('tuplet-bracket', references, ((p, y + 3), (p + (q - p) / 3, y - 4), (q - (q - p) / 3, y - 4), (q, y + 3)), 1.3))
|
||||
else:
|
||||
result.append(line('tuplet-bracket', references, ((p, y), (q, y)), 1.3))
|
||||
if a.start >= start:
|
||||
result.append(line('tuplet-bracket', references, ((x1, y), (x1, y + 5)), 1.3))
|
||||
if b.end <= end:
|
||||
result.append(line('tuplet-bracket', references, ((x2, y), (x2, y + 5)), 1.3))
|
||||
outer_top = min(top, bounds(result).top if result else top)
|
||||
for span in index.staff.get('octave_shifts', []):
|
||||
a, b = fraction(span['start']), fraction(span['end'])
|
||||
if b <= start or a >= end:
|
||||
continue
|
||||
members = [n for n in anchors if a <= n.time < b]
|
||||
x1 = min((n.x for n in members), default=left) if a >= start else left
|
||||
x2 = max((n.x + style.space * 2 for n in members), default=right) if b <= end else right
|
||||
above = span['octaves'] < 0
|
||||
y = outer_top - 18 if above else max(bounds(items + result).bottom, 0) + 24
|
||||
label = str(abs(span['octaves']) * 7 + 1) + ('va' if above else 'vb')
|
||||
text = fonts.text(label, x1, y, 16, 'octave-shift', (span['id'],))
|
||||
result.append(text)
|
||||
outer_top = min(outer_top, text.box.top - 4)
|
||||
cursor = text.box.right + 4
|
||||
while cursor < x2:
|
||||
result.append(line('octave-shift', (span['id'],), ((cursor, y - 5), (min(cursor + 6, x2), y - 5)), 1))
|
||||
cursor += 10
|
||||
if b <= end:
|
||||
result.append(line('octave-shift', (span['id'],), ((x2, y - 5), (x2, y + (2 if above else -12))), 1))
|
||||
for event in index.events:
|
||||
tremolo = event.value.get('tremolo', {})
|
||||
if tremolo.get('type') != 'start':
|
||||
continue
|
||||
partner = index.by_id[tremolo['partner']]
|
||||
if event.start >= end or partner.end <= start:
|
||||
continue
|
||||
a, b = by_event.get(event.value['id']), by_event.get(partner.value['id'])
|
||||
if not a and not b:
|
||||
continue
|
||||
anchor = a or b
|
||||
x1, x2 = a.stem_x + 4 if a else left, b.stem_x - 4 if b else right
|
||||
y = anchor.stem_y - anchor.direction * 10
|
||||
for mark in range(tremolo['marks']):
|
||||
result.append(line('tremolo', (event.value['id'], partner.value['id']), ((x1, y + mark * 6), (x2, y + mark * 6)), 3.5))
|
||||
return items + result
|
||||
|
||||
def place_text_lanes(items: list[Primitive], fonts: Fonts, style: Style) -> list[Primitive]:
|
||||
music = [p for p in items if p.role not in ('harmony', 'lyric', 'section', 'tempo')]
|
||||
box = bounds(music)
|
||||
result = list(music)
|
||||
top = box.top - 12
|
||||
for role in ('harmony', 'tempo', 'section'):
|
||||
lane = [p for p in items if p.role == role]
|
||||
if lane:
|
||||
extent = bounds(lane)
|
||||
result.extend(p.moved(0, top - extent.bottom) for p in lane)
|
||||
top -= extent.bottom - extent.top + 10
|
||||
lyrics = [p for p in items if p.role == 'lyric']
|
||||
if lyrics:
|
||||
result.extend(p.moved(0, max(box.bottom, 0) + 20) for p in lyrics)
|
||||
return result
|
||||
|
||||
def draw_lyric_spans(index: ScoreIndex, items: list[Primitive], anchors: list[Anchor], start: Fraction, end: Fraction, left: float, right: float, fonts: Fonts) -> list[Primitive]:
|
||||
result = list(items)
|
||||
labels = {p.sources[0]: p for p in items if p.role == 'lyric'}
|
||||
positions = {a.event: a for a in anchors}
|
||||
lyric_baseline = None
|
||||
for event in index.events:
|
||||
for verse in event.value.get('lyrics', []):
|
||||
label = labels.get(event.value['id'] + ':lyric:' + verse['number'])
|
||||
if label:
|
||||
lyric_baseline = label.points[0][1] - index.lyric_lanes.index((event.voice, verse['number'])) * 23
|
||||
break
|
||||
if lyric_baseline is not None:
|
||||
break
|
||||
if lyric_baseline is None:
|
||||
lyric_baseline = max(0, bounds(items).bottom) + 20
|
||||
for voice, number in index.lyric_lanes:
|
||||
verses = [(e, v) for e in index.events if e.voice == voice and not e.value.get('hidden') for v in e.value.get('lyrics', []) if v['number'] == number]
|
||||
for i, (event, verse) in enumerate(verses):
|
||||
following = verses[i + 1] if i + 1 < len(verses) else None
|
||||
syllabic = next((s['text'] for s in verse['syllables'] if s['kind'] == 'syllabic'), 'single')
|
||||
extend = next((s['attributes'].get('type', 'start') for s in verse['syllables'] if s['kind'] == 'extend'), None)
|
||||
if syllabic not in ('begin', 'middle') and extend not in ('start', 'continue'):
|
||||
continue
|
||||
target = following[0].start if following else max(e.end for e in index.events if e.voice == voice)
|
||||
if target < start or event.start >= end:
|
||||
continue
|
||||
identifier = event.value['id'] + ':lyric:' + number
|
||||
next_id = following[0].value['id'] + ':lyric:' + number if following else ''
|
||||
a, b = labels.get(identifier), labels.get(next_id)
|
||||
x1 = a.box.right + 5 if a else left
|
||||
x2 = b.box.left - 5 if b else right
|
||||
if b is None and following and following[0].value['id'] in positions:
|
||||
x2 = positions[following[0].value['id']].x
|
||||
y = lyric_baseline + index.lyric_lanes.index((voice, number)) * 23
|
||||
if x2 <= x1:
|
||||
continue
|
||||
if extend in ('start', 'continue'):
|
||||
result.append(line('lyric-extension', (identifier,), ((x1, y + 3), (x2, y + 3)), 1))
|
||||
elif following:
|
||||
count = max(1, int((x2 - x1) / 90))
|
||||
for dash in range(count):
|
||||
result.append(fonts.text('-', x1 + (x2 - x1) * (dash + 1) / (count + 1), y, 18, 'lyric-hyphen', (identifier,), True))
|
||||
return result
|
||||
Reference in New Issue
Block a user