Files
product-vamp/generator/engraving/notes.py
T
khodowany 7a83b5435f
ci / checks (push) Successful in 45m44s
Engraver
2026-09-16 18:17:42 -05:00

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