165 lines
10 KiB
Python
165 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
from fractions import Fraction
|
|
|
|
from generator.model import alteration, fraction
|
|
from generator.engraving.fonts import Fonts
|
|
from generator.engraving.notation import Event, ScoreIndex, accidental, flags, fragment_written, head_glyph
|
|
from generator.engraving.types import Anchor, Fragment, Primitive, Style, bounds, line
|
|
|
|
def dots(fonts: Fonts, count: int, x: float, y: float, size: float, sources: tuple[str, ...]) -> list[Primitive]:
|
|
result = []
|
|
for i in range(count):
|
|
result.append(fonts.glyph('augmentationDot', x + i * size * 0.6, y, size, 'dot', sources))
|
|
return result
|
|
|
|
def rest_glyph(kind: str) -> str:
|
|
return {'maxima': 'restMaxima', 'long': 'restLonga', 'breve': 'restDoubleWhole', 'whole': 'restWhole', 'half': 'restHalf', 'quarter': 'restQuarter', 'eighth': 'rest8th'}.get(kind, 'rest' + kind)
|
|
|
|
def time_modification(written: dict, items: list[Primitive], fonts: Fonts, sources: tuple[str, ...]) -> None:
|
|
ratio = fraction(written.get('ratio', {'numerator': 1, 'denominator': 1}))
|
|
if ratio != 1 and items:
|
|
box = bounds(items)
|
|
items.append(fonts.text(f'{ratio.denominator}:{ratio.numerator}', (box.left + box.right) / 2, box.top - 6, 13, 'time-modification', sources, True))
|
|
|
|
def draw_event(index: ScoreIndex, event: Event, start: Fraction, end: Fraction, fonts: Fonts, style: Style) -> tuple[list[Primitive], list[Anchor], list[Fragment]]:
|
|
note = event.value
|
|
onset = max(start, event.start)
|
|
finish = min(end, event.end)
|
|
visible = not note.get('hidden') and note['kind'] != 'spacer'
|
|
references = (note['id'],)
|
|
size = style.space * (0.7 if note.get('cue') or note.get('grace') else 1)
|
|
items, anchors, fragments = [], [], []
|
|
state = index.state(onset)
|
|
lines = state['staff_details']['lines']
|
|
if not note['pitches']:
|
|
written = fragment_written(note['written'], event.start, event.end, onset, finish)
|
|
y = -style.space * (lines - 1) / 2
|
|
if position := note.get('rest_position'):
|
|
y = -index.pitch_position(position, onset) * style.space / 2
|
|
elif event.lanes > 1:
|
|
y += (-1 if event.lane % 2 == 0 else 1) * (event.lane // 2 + 1) * style.space * 3
|
|
kind = 'slash' if note.get('notehead') == 'slash' else 'rest'
|
|
fragment = Fragment(note['id'], note['id'], onset, finish, written['type'], written.get('dots', 0), fraction(written.get('ratio', {'numerator': 1, 'denominator': 1})), visible, kind, None)
|
|
fragments.append(fragment)
|
|
if visible:
|
|
glyph = head_glyph(written, 'slash') if kind == 'slash' else rest_glyph('whole' if note.get('measure_rest') and onset == event.start and finish == event.end else written['type'])
|
|
if written['type'] == 'whole' and not note.get('rest_position') and event.lanes == 1:
|
|
y -= style.space
|
|
items.append(fonts.glyph(glyph, 0, y, size, kind, references))
|
|
items.extend(dots(fonts, written.get('dots', 0), items[0].box.right + 4, y - size / 2, size, references))
|
|
if kind == 'slash' and written['type'] not in ('whole', 'breve', 'long', 'maxima'):
|
|
stem_x = items[0].box.right
|
|
stem_y = y - 3 * size
|
|
items.append(line('stem', references, ((stem_x, y), (stem_x, stem_y))))
|
|
if flags(written):
|
|
flag = 'flag' + ('8th' if written['type'] == 'eighth' else written['type']) + 'Up'
|
|
items.append(fonts.glyph(flag, stem_x, stem_y, size, 'flag', references))
|
|
anchors.append(Anchor(note['id'], note['id'], event.voice, onset, finish, 0, y, 0, y - size * 3, -1, 0, fragment))
|
|
if not note.get('tuplet_ids') or written != note['written']:
|
|
time_modification(written, items, fonts, references)
|
|
return items, anchors, fragments
|
|
groups = {}
|
|
for tone in note['pitches']:
|
|
tone_end = event.start + fraction(tone['duration'])
|
|
if tone_end <= start and not note.get('grace'):
|
|
continue
|
|
written = fragment_written(tone['written'], event.start, tone_end, onset, min(end, tone_end))
|
|
stem = tone.get('stem', note.get('stem', 'auto'))
|
|
identity = (written['type'], written.get('dots', 0), str(written.get('ratio')), stem)
|
|
groups.setdefault(identity, []).append((tone, written, tone_end))
|
|
group_x = 0
|
|
for members in groups.values():
|
|
positioned = sorted(((index.pitch_position(tone, onset), tone, written, tone_end) for tone, written, tone_end in members), key=lambda item: item[0])
|
|
stem = members[0][0].get('stem', note.get('stem', 'auto'))
|
|
direction = -1 if stem in ('up', 'double') else 1 if stem == 'down' else (-1 if event.lane % 2 == 0 else 1) if event.lanes > 1 else (1 if sum(p[0] for p in positioned) / len(positioned) >= lines - 1 else -1)
|
|
if direction == 1:
|
|
positioned.reverse()
|
|
group_items, group_anchors = [], []
|
|
previous = None
|
|
displaced = False
|
|
accidental_boxes = []
|
|
for position, tone, written, tone_end in positioned:
|
|
sources = (note['id'], tone['id'])
|
|
tone_size = size * (0.7 if tone.get('cue') and not note.get('cue') else 1)
|
|
glyph = head_glyph(written, tone.get('notehead', note.get('notehead')))
|
|
displayed = visible and tone.get('print', {}).get('print-object') != 'no' and bool(glyph)
|
|
y = -position * style.space / 2
|
|
if previous is not None and abs(position - previous) < 2:
|
|
displaced = not displaced
|
|
else:
|
|
displaced = False
|
|
x = group_x + (tone_size * 1.2 * -direction if displaced else 0)
|
|
previous = position
|
|
fragment = Fragment(tone['id'], note['id'], onset, min(end, tone_end), written['type'], written.get('dots', 0), fraction(written.get('ratio', {'numerator': 1, 'denominator': 1})), displayed, 'tone', position)
|
|
fragments.append(fragment)
|
|
if not displayed:
|
|
continue
|
|
if state['clef']['sign'] == 'TAB':
|
|
head = fonts.text(str(tone['tablature']['fret']), x, y + 5, tone_size * 1.5, 'tab', sources)
|
|
elif written['type'] in ('long', 'maxima') and tone.get('notehead', {}).get('name', 'normal') == 'normal':
|
|
width = tone_size * (2.8 if written['type'] == 'maxima' else 1.4)
|
|
head = line('head', sources, ((x, y - tone_size / 2), (x + width, y - tone_size / 2), (x + width, y + tone_size / 2), (x, y + tone_size / 2), (x, y - tone_size / 2)), 2)
|
|
else:
|
|
head = fonts.glyph(glyph, x, y, tone_size, 'head', sources)
|
|
group_items.append(head)
|
|
if tone.get('notehead', {}).get('parentheses') == 'yes':
|
|
group_items.extend((fonts.text('(', head.box.left - 6, y + 5, 18, 'head-parentheses', sources), fonts.text(')', head.box.right + 2, y + 5, 18, 'head-parentheses', sources)))
|
|
if state['clef']['sign'] != 'TAB':
|
|
ledger = -2
|
|
while position <= ledger:
|
|
group_items.append(line('ledger', sources, ((head.box.left - 5, -ledger * style.space / 2), (head.box.right + 5, -ledger * style.space / 2)), 1.8))
|
|
ledger -= 2
|
|
ledger = lines * 2
|
|
while position >= ledger:
|
|
group_items.append(line('ledger', sources, ((head.box.left - 5, -ledger * style.space / 2), (head.box.right + 5, -ledger * style.space / 2)), 1.8))
|
|
ledger += 2
|
|
if 'alter' in tone and index.accidentals[tone['id']] and onset == event.start:
|
|
accidental_items = accidental(fonts, alteration(tone['alter']), tone.get('accidental_display', {}), tone_size, sources)
|
|
box = bounds(accidental_items)
|
|
dx = min(group_x, head.box.left) - 5 - box.right
|
|
candidate = box.moved(dx, y)
|
|
while any(candidate.overlaps(other, 2) for other in accidental_boxes):
|
|
dx -= box.right - box.left + 4
|
|
candidate = box.moved(dx, y)
|
|
accidental_boxes.append(candidate)
|
|
group_items.extend(p.moved(dx, y) for p in accidental_items)
|
|
if tone.get('print', {}).get('print-dot') != 'no':
|
|
dot_y = y - style.space / 2 if position % 2 == 0 else y
|
|
group_items.extend(dots(fonts, written.get('dots', 0), head.box.right + 5, dot_y, tone_size, sources))
|
|
if head.kind == 'glyph':
|
|
stem_x = x + fonts.anchor(glyph, 'stemUpSE' if direction == -1 else 'stemDownNW', tone_size)[0]
|
|
else:
|
|
stem_x = head.box.right - 0.6 if direction == -1 else head.box.left + 0.6
|
|
stem_y = y + direction * max(3.5 * size, (flags(written) + 1) * size * 0.75)
|
|
group_anchors.append(Anchor(tone['id'], note['id'], event.voice, onset, min(end, tone_end), x, y, stem_x, stem_y, direction, flags(written), fragment))
|
|
if group_anchors:
|
|
stem_y = min(a.stem_y for a in group_anchors) if direction == -1 else max(a.stem_y for a in group_anchors)
|
|
anchor = min(group_anchors, key=lambda a: a.y) if direction == 1 else max(group_anchors, key=lambda a: a.y)
|
|
written = members[0][1]
|
|
if stem != 'none' and written['type'] not in ('whole', 'breve'):
|
|
group_items.append(line('stem', references, ((anchor.stem_x, anchor.y), (anchor.stem_x, stem_y)), size * 0.12))
|
|
if stem == 'double':
|
|
other_x = anchor.x if direction == -1 else anchor.x + size * 1.3
|
|
other_y = anchor.y - direction * size * 3.5
|
|
group_items.append(line('stem', references, ((other_x, anchor.y), (other_x, other_y)), size * 0.12))
|
|
if flags(written):
|
|
flag = 'flag' + ('8th' if written['type'] == 'eighth' else written['type']) + ('Down' if direction == -1 else 'Up')
|
|
group_items.append(fonts.glyph(flag, other_x, other_y, size, 'flag', references))
|
|
if flags(written):
|
|
flag = 'flag' + ('8th' if written['type'] == 'eighth' else written['type']) + ('Up' if direction == -1 else 'Down')
|
|
group_items.append(fonts.glyph(flag, anchor.stem_x, stem_y, size, 'flag', references))
|
|
if note.get('grace', {}).get('slash'):
|
|
group_items.append(line('grace-slash', references, ((anchor.stem_x - 5, stem_y + 14), (anchor.stem_x + 9, stem_y + 4)), 1.5))
|
|
if note.get('tremolo', {}).get('type') == 'single':
|
|
for mark in range(note['tremolo']['marks']):
|
|
ty = stem_y - direction * (10 + mark * 5)
|
|
group_items.append(line('tremolo', references, ((anchor.stem_x - 6, ty + 2), (anchor.stem_x + 6, ty - 2)), 3))
|
|
from dataclasses import replace
|
|
anchors.extend(replace(a, stem_x=anchor.stem_x, stem_y=stem_y) for a in group_anchors)
|
|
if not note.get('tuplet_ids') or members[0][1] != members[0][0]['written']:
|
|
time_modification(members[0][1], group_items, fonts, references)
|
|
items.extend(group_items)
|
|
group_x = max((p.box.right for p in items), default=group_x) + size
|
|
return items, anchors, fragments
|