from __future__ import annotations import dataclasses import fractions import generator.engraving.fonts import generator.engraving.notation import generator.engraving.types import generator.model def dots(fonts: generator.engraving.fonts.Fonts, count: int, x: float, y: float, size: float, sources: tuple[str, ...]) -> list[generator.engraving.types.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[generator.engraving.types.Primitive], fonts: generator.engraving.fonts.Fonts, sources: tuple[str, ...]) -> None: ratio = generator.model.fraction(written.get('ratio', {'numerator': 1, 'denominator': 1})) if ratio != 1 and items: box = generator.engraving.types.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: generator.engraving.notation.ScoreIndex, event: generator.engraving.notation.Event, start: fractions.Fraction, end: fractions.Fraction, fonts: generator.engraving.fonts.Fonts, style: generator.engraving.types.Style) -> tuple[list[generator.engraving.types.Primitive], list[generator.engraving.types.Anchor], list[generator.engraving.types.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 = generator.engraving.notation.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: separation = 1 if index.staff.get('tablature') and event.lanes == 2 else 3 y += (-1 if event.lane % 2 == 0 else 1) * (event.lane // 2 + 1) * style.space * separation kind = 'slash' if note.get('notehead') == 'slash' else 'rest' fragment = generator.engraving.types.Fragment(note['id'], note['id'], onset, finish, written['type'], written.get('dots', 0), generator.model.fraction(written.get('ratio', {'numerator': 1, 'denominator': 1})), visible, kind, None) fragments.append(fragment) if visible: glyph = generator.engraving.notation.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(generator.engraving.types.line('stem', references, ((stem_x, y), (stem_x, stem_y)))) if generator.engraving.notation.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(generator.engraving.types.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 + generator.model.fraction(tone['duration']) if tone_end <= start and not note.get('grace'): continue written = generator.engraving.notation.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 = generator.engraving.notation.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 = generator.engraving.types.Fragment(tone['id'], note['id'], onset, min(end, tone_end), written['type'], written.get('dots', 0), generator.model.fraction(written.get('ratio', {'numerator': 1, 'denominator': 1})), displayed, 'tone', position) fragments.append(fragment) if not displayed: continue if 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 = generator.engraving.types.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))) ledger = -2 while position <= ledger: group_items.append(generator.engraving.types.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(generator.engraving.types.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 = generator.engraving.notation.accidental(fonts, generator.model.alteration(tone['alter']), tone.get('accidental_display', {}), tone_size, sources) box = generator.engraving.types.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, (generator.engraving.notation.flags(written) + 1) * size * 0.75) group_anchors.append(generator.engraving.types.Anchor(tone['id'], note['id'], event.voice, onset, min(end, tone_end), x, y, stem_x, stem_y, direction, generator.engraving.notation.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(generator.engraving.types.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(generator.engraving.types.line('stem', references, ((other_x, anchor.y), (other_x, other_y)), size * 0.12)) if generator.engraving.notation.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 generator.engraving.notation.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(generator.engraving.types.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(generator.engraving.types.line('tremolo', references, ((anchor.stem_x - 6, ty + 2), (anchor.stem_x + 6, ty - 2)), 3)) anchors.extend(dataclasses.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