from __future__ import annotations import fractions import generator.engraving.fonts import generator.engraving.notation import generator.engraving.types import generator.model def beam_groups(index: generator.engraving.notation.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 = index.measure_starts_by_id[event.measure['id']] beats = generator.model.beat_lengths(index.state(event.start)['meter']) offset = event.start - measure_start beat = 0 edge = fractions.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 generator.engraving.notation.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: generator.engraving.notation.ScoreIndex, anchors: list[generator.engraving.types.Anchor], items: list[generator.engraving.types.Primitive], start: fractions.Fraction, end: fractions.Fraction, left: float, right: float, style: generator.engraving.types.Style) -> list[generator.engraving.types.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(generator.engraving.types.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(generator.engraving.types.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(generator.engraving.types.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: generator.engraving.notation.ScoreIndex, anchors: list[generator.engraving.types.Anchor], items: list[generator.engraving.types.Primitive], start: fractions.Fraction, end: fractions.Fraction, left: float, right: float, fonts: generator.engraving.fonts.Fonts, style: generator.engraving.types.Style) -> list[generator.engraving.types.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 + generator.model.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(generator.engraving.types.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(generator.engraving.types.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', 'scale', '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(generator.engraving.types.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(generator.engraving.types.line('tuplet-bracket', references, ((p, y), (q, y)), 1.3)) if a.start >= start: result.append(generator.engraving.types.line('tuplet-bracket', references, ((x1, y), (x1, y + 5)), 1.3)) if b.end <= end: result.append(generator.engraving.types.line('tuplet-bracket', references, ((x2, y), (x2, y + 5)), 1.3)) outer_top = min(top, generator.engraving.types.bounds(result).top if result else top) for span in index.staff.get('octave_shifts', []): a, b = generator.model.fraction(span['start']), generator.model.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(generator.engraving.types.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(generator.engraving.types.line('octave-shift', (span['id'],), ((cursor, y - 5), (min(cursor + 6, x2), y - 5)), 1)) cursor += 10 if b <= end: result.append(generator.engraving.types.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(generator.engraving.types.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[generator.engraving.types.Primitive], fonts: generator.engraving.fonts.Fonts, style: generator.engraving.types.Style) -> list[generator.engraving.types.Primitive]: music = [p for p in items if p.role not in ('harmony', 'lyric', 'section', 'scale')] box = generator.engraving.types.bounds(music) result = list(music) top = box.top - 12 for role in ('harmony', 'scale', 'section'): lane = [p for p in items if p.role == role] if lane: extent = generator.engraving.types.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: generator.engraving.notation.ScoreIndex, items: list[generator.engraving.types.Primitive], anchors: list[generator.engraving.types.Anchor], start: fractions.Fraction, end: fractions.Fraction, left: float, right: float, fonts: generator.engraving.fonts.Fonts) -> list[generator.engraving.types.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, generator.engraving.types.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 if 'extend_to' in verse and extend != 'start': continue target = following[0].start if following else max(e.end for e in index.events if e.voice == voice) if 'extend_to' in verse: target = generator.model.fraction(verse['extend_to']) 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 if 'extend_to' in verse: endpoints = [a.x for a in anchors if a.time >= target] x2 = min(endpoints) - 5 if endpoints else right y = lyric_baseline + index.lyric_lanes.index((voice, number)) * 23 if x2 <= x1: continue if extend in ('start', 'continue'): result.append(generator.engraving.types.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