@@ -1,33 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from bisect import bisect_left, bisect_right
|
||||
from fractions import Fraction
|
||||
import bisect
|
||||
import collections
|
||||
import dataclasses
|
||||
import fractions
|
||||
import math
|
||||
|
||||
from generator import harmony
|
||||
from generator.engraving import tablature
|
||||
from generator.model import validate_document
|
||||
from generator.engraving.attributes import barline, staff_attributes, tempo
|
||||
from generator.engraving.fonts import Fonts
|
||||
from generator.engraving.notation import ScoreIndex
|
||||
from generator.engraving.notes import draw_event
|
||||
from generator.engraving.spans import draw_beams, draw_lyric_spans, draw_spans, place_text_lanes
|
||||
from generator.engraving.types import Anchor, EngravingError, Fragment, LayoutDocument, Page, Primitive, Style, System, bounds, line
|
||||
import generator.engraving.attributes
|
||||
import generator.engraving.audit
|
||||
import generator.engraving.fonts
|
||||
import generator.engraving.notation
|
||||
import generator.engraving.notes
|
||||
import generator.engraving.spans
|
||||
import generator.engraving.tablature
|
||||
import generator.engraving.types
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
|
||||
def lyric_text(verse: dict) -> str:
|
||||
return ''.join(s['text'] for s in verse['syllables'] if s['kind'] in ('text', 'elision'))
|
||||
|
||||
def column(index: ScoreIndex, time: Fraction, start: Fraction, end: Fraction, fonts: Fonts, style: Style) -> tuple[list[Primitive], list[Anchor], list[Fragment]]:
|
||||
def column(index: generator.engraving.notation.ScoreIndex, time: fractions.Fraction, 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]]:
|
||||
items, anchors, fragments = [], [], []
|
||||
events = index.events_at(time, include_sustained=time == start)
|
||||
grace_width = 0
|
||||
occupied = []
|
||||
for event in events:
|
||||
try:
|
||||
drawn, placed, pieces = draw_event(index, event, start, end, fonts, style)
|
||||
drawn, placed, pieces = generator.engraving.notes.draw_event(index, event, start, end, fonts, style)
|
||||
except ValueError as error:
|
||||
raise EngravingError(f'measure {event.measure["number"]}, voice {event.voice}, {event.value["id"]}: {error}') from error
|
||||
raise generator.engraving.types.EngravingError(f'measure {event.measure["number"]}, voice {event.voice}, {event.value["id"]}: {error}') from error
|
||||
x = grace_width
|
||||
collidable = [p for p in drawn if p.role in ('head', 'tab', 'rest', 'slash', 'accidental', 'dot')]
|
||||
while any(p.box.moved(x, 0).overlaps(q.box, 2) for p in collidable for q in occupied):
|
||||
@@ -38,7 +40,7 @@ def column(index: ScoreIndex, time: Fraction, start: Fraction, end: Fraction, fo
|
||||
grace_width = max((p.box.right for p in drawn), default=grace_width) + 9
|
||||
occupied.extend(p for p in drawn if p.role in ('head', 'tab', 'rest', 'slash', 'accidental', 'dot'))
|
||||
items.extend(drawn)
|
||||
items.extend(tablature.column(index, event, placed, fonts, style))
|
||||
items.extend(generator.engraving.tablature.column(index, event, placed, fonts, style))
|
||||
anchors.extend(placed)
|
||||
fragments.extend(pieces)
|
||||
if event.start == time and not event.value.get('hidden'):
|
||||
@@ -51,47 +53,61 @@ def column(index: ScoreIndex, time: Fraction, start: Fraction, end: Fraction, fo
|
||||
measure_start, _, measure = index.measure_at(time)
|
||||
for owner, _, item in index.harmonies_by_onset.get(time, []):
|
||||
if owner is measure:
|
||||
items.append(fonts.text(harmony.symbol(item), 0, 0, 21, 'harmony', (item['id'],)))
|
||||
items.append(fonts.text(generator.harmony.symbol(item), 0, 0, 21, 'harmony', (item['id'],)))
|
||||
for owner, i, value in index.tempos_by_onset.get(time, []):
|
||||
if owner is measure:
|
||||
items.extend(tempo(value, fonts, 0, 0, style.space, measure['id'] + f':tempo:{i}'))
|
||||
items.extend(generator.engraving.attributes.tempo(value, fonts, 0, 0, style.space, measure['id'] + f':tempo:{i}'))
|
||||
if time == measure_start:
|
||||
items.append(fonts.text(str(measure['number']), 0, -style.space * (index.state(time)['staff_details']['lines'] + 0.5), 12, 'measure-number', (measure['id'],)))
|
||||
if label := measure.get('section_label'):
|
||||
items.append(fonts.text(label, 0, 0, 19, 'section', (measure['id'] + ':section',)))
|
||||
return items, anchors, fragments
|
||||
|
||||
def make_system(index: ScoreIndex, start: Fraction, end: Fraction, fonts: Fonts, style: Style, justify: bool = False) -> System:
|
||||
def column_metrics(index, time, start, end, fonts, style, columns):
|
||||
events = index.events_at(time, include_sustained=time == start)
|
||||
finish = min(end, max((event.end for event in events), default=time))
|
||||
key = (time, start, finish)
|
||||
if columns is not None and key in columns:
|
||||
columns.move_to_end(key)
|
||||
return columns[key]
|
||||
drawn, placed, pieces = column(index, time, start, end, fonts, style)
|
||||
result = drawn, placed, pieces, generator.engraving.types.bounds(drawn)
|
||||
if columns is not None:
|
||||
columns[key] = result
|
||||
if len(columns) > 256:
|
||||
columns.popitem(last=False)
|
||||
return result
|
||||
|
||||
def make_system(index: generator.engraving.notation.ScoreIndex, start: fractions.Fraction, end: fractions.Fraction, fonts: generator.engraving.fonts.Fonts, style: generator.engraving.types.Style, justify: bool = False, *, columns=None) -> generator.engraving.types.System:
|
||||
items, anchors, fragments = [], [], []
|
||||
state = index.state(start)
|
||||
measure = index.measure_at(start)[2]
|
||||
header = staff_attributes(index, state, start, fonts, style.space, measure['id'] + ':initial', ('clef', 'key', 'meter', 'staff_details'))
|
||||
header = generator.engraving.attributes.staff_attributes(index, state, start, fonts, style.space, measure['id'] + ':initial', ('clef', 'key', 'meter', 'staff_details'))
|
||||
items.extend(header)
|
||||
cursor = max((p.box.right for p in header), default=0) + 22
|
||||
content_left = cursor - 9
|
||||
times = [start] + index.column_times[bisect_right(index.column_times, start):bisect_left(index.column_times, end)]
|
||||
times = [start] + index.column_times[bisect.bisect_right(index.column_times, start):bisect.bisect_left(index.column_times, end)]
|
||||
placements = []
|
||||
groups = []
|
||||
for ti, time in enumerate(times):
|
||||
item_start, anchor_start = len(items), len(anchors)
|
||||
drawn, placed, pieces = column(index, time, start, end, fonts, style)
|
||||
drawn, placed, pieces, box = column_metrics(index, time, start, end, fonts, style, columns)
|
||||
leading = []
|
||||
measure_start, _, m = index.measure_at(time)
|
||||
if time == measure_start and time > start:
|
||||
previous = index.measure_at(time, before=True)[2]
|
||||
leading.extend(barline(previous.get('barlines', {}).get('right', 'regular'), 0, -style.space * (index.state(time)['staff_details']['lines'] - 1), previous['id']))
|
||||
leading.extend(generator.engraving.attributes.barline(previous.get('barlines', {}).get('right', 'regular'), 0, -style.space * (index.state(time)['staff_details']['lines'] - 1), previous['id']))
|
||||
old_state = index.state(time, before=True)
|
||||
fields = tuple(k for k in ('clef', 'key', 'meter', 'staff_details') if old_state[k] != index.state(time)[k])
|
||||
leading.extend(p.moved(12, 0) for p in staff_attributes(index, index.state(time), time, fonts, style.space, m['id'] + ':initial', fields))
|
||||
leading.extend(p.moved(12, 0) for p in generator.engraving.attributes.staff_attributes(index, index.state(time), time, fonts, style.space, m['id'] + ':initial', fields))
|
||||
if time > start:
|
||||
for owner, ci, change in index.attributes_by_onset.get(time, []):
|
||||
extra = staff_attributes(index, index.state(time), time, fonts, style.space, owner['id'] + f':attributes:{ci}', tuple(change))
|
||||
extra = generator.engraving.attributes.staff_attributes(index, index.state(time), time, fonts, style.space, owner['id'] + f':attributes:{ci}', tuple(change))
|
||||
leading.extend(p.moved(max((p.box.right for p in leading), default=0) + 10, 0) for p in extra)
|
||||
if leading:
|
||||
width = bounds(leading).right - bounds(leading).left + 14
|
||||
items.extend(p.moved(cursor - bounds(leading).left, 0) for p in leading)
|
||||
width = generator.engraving.types.bounds(leading).right - generator.engraving.types.bounds(leading).left + 14
|
||||
items.extend(p.moved(cursor - generator.engraving.types.bounds(leading).left, 0) for p in leading)
|
||||
cursor += width
|
||||
box = bounds(drawn)
|
||||
x = cursor - min(0, box.left)
|
||||
placements.append((time, x))
|
||||
items.extend(p.moved(x, 0) for p in drawn)
|
||||
@@ -114,10 +130,10 @@ def make_system(index: ScoreIndex, start: Fraction, end: Fraction, fonts: Fonts,
|
||||
last_measure = index.measure_at(end, before=True)[2]
|
||||
tab_items = [p for p in items if p.role.startswith('tab')]
|
||||
items = [p for p in items if not p.role.startswith('tab')]
|
||||
items = draw_beams(index, anchors, items, start, end, content_left, right, style)
|
||||
items = draw_spans(index, anchors, items, start, end, content_left, right, fonts, style)
|
||||
items = place_text_lanes(items, fonts, style)
|
||||
items = draw_lyric_spans(index, items, anchors, start, end, content_left, right, fonts)
|
||||
items = generator.engraving.spans.draw_beams(index, anchors, items, start, end, content_left, right, style)
|
||||
items = generator.engraving.spans.draw_spans(index, anchors, items, start, end, content_left, right, fonts, style)
|
||||
items = generator.engraving.spans.place_text_lanes(items, fonts, style)
|
||||
items = generator.engraving.spans.draw_lyric_spans(index, items, anchors, start, end, content_left, right, fonts)
|
||||
changes = [(0, state['staff_details']['lines'])]
|
||||
for time, x in placements:
|
||||
lines = index.state(time)['staff_details']['lines']
|
||||
@@ -128,18 +144,18 @@ def make_system(index: ScoreIndex, start: Fraction, end: Fraction, fonts: Fonts,
|
||||
x2 = changes[i + 1][0] if i + 1 < len(changes) else width - 1
|
||||
for number in range(lines):
|
||||
y = -number * style.space
|
||||
staff_lines.append(line('staff', (), ((x1, y), (x2, y)), style.space * 0.13))
|
||||
staff_lines.append(generator.engraving.types.line('staff', (), ((x1, y), (x2, y)), style.space * 0.13))
|
||||
items = staff_lines + items
|
||||
if end in index.boundaries:
|
||||
items.extend(barline(last_measure.get('barlines', {}).get('right', 'regular'), width - 1, -style.space * (index.state(end, before=True)['staff_details']['lines'] - 1), last_measure['id']))
|
||||
items.extend(generator.engraving.attributes.barline(last_measure.get('barlines', {}).get('right', 'regular'), width - 1, -style.space * (index.state(end, before=True)['staff_details']['lines'] - 1), last_measure['id']))
|
||||
if start in index.boundaries:
|
||||
items.extend(barline(measure.get('barlines', {}).get('left', 'none'), 0, -style.space * (state['staff_details']['lines'] - 1), measure['id']))
|
||||
items.extend(generator.engraving.attributes.barline(measure.get('barlines', {}).get('left', 'none'), 0, -style.space * (state['staff_details']['lines'] - 1), measure['id']))
|
||||
else:
|
||||
items.append(fonts.text(str(measure['number']) + ' (cont.)', 0, min(p.box.top for p in items) - 6, 12, 'continuation', (measure['id'],)))
|
||||
items = tablature.staff(index, items + tab_items, start, end, content_left, width - 1, fonts, style)
|
||||
return System(start, end, tuple(items), tuple(anchors), tuple(fragments), bounds(items))
|
||||
items = generator.engraving.tablature.staff(index, items + tab_items, start, end, content_left, width - 1, fonts, style)
|
||||
return generator.engraving.types.System(start, end, tuple(items), tuple(anchors), tuple(fragments), generator.engraving.types.bounds(items))
|
||||
|
||||
def paginate(systems: list[System], document: dict, fonts: Fonts, style: Style) -> tuple[Page, ...]:
|
||||
def paginate(systems: list[generator.engraving.types.System], document: dict, fonts: generator.engraving.fonts.Fonts, style: generator.engraving.types.Style) -> tuple[generator.engraving.types.Page, ...]:
|
||||
pages = []
|
||||
placed = []
|
||||
header = []
|
||||
@@ -154,10 +170,10 @@ def paginate(systems: list[System], document: dict, fonts: Fonts, style: Style)
|
||||
size -= 1
|
||||
item = fonts.text(title, style.margin_x, style.margin_y + size, size, 'title', ())
|
||||
if item.box.right > style.width - style.margin_x:
|
||||
raise EngravingError('score title cannot fit the page without clipping')
|
||||
raise generator.engraving.types.EngravingError('score title cannot fit the page without clipping')
|
||||
result = [item]
|
||||
if number == 1:
|
||||
subtitle = ' - '.join(str(value) for value in (document['metadata'].get('composer'), document['staves'][0].get('name'), document['metadata'].get('style')) if value)
|
||||
subtitle = ' - '.join(str(value) for value in (document['metadata'].get('composer'), document['staves'][0].get('name')) if value)
|
||||
if subtitle:
|
||||
result.append(fonts.text(subtitle, style.margin_x, item.box.bottom + 22, 16, 'subtitle', ()))
|
||||
return result
|
||||
@@ -166,61 +182,65 @@ def paginate(systems: list[System], document: dict, fonts: Fonts, style: Style)
|
||||
height = system.box.bottom - system.box.top
|
||||
if first:
|
||||
header = page_header(len(pages) + 1)
|
||||
cursor = bounds(header).bottom + 24
|
||||
cursor = generator.engraving.types.bounds(header).bottom + 24
|
||||
first = False
|
||||
if cursor + height > style.height - style.margin_y - 18:
|
||||
if not placed:
|
||||
raise EngravingError(f'at musical offset {system.start}: simultaneous notation needs {height:.1f}px vertically; fixed-size page cannot fit it')
|
||||
raise generator.engraving.types.EngravingError(f'at musical offset {system.start}: simultaneous notation needs {height:.1f}px vertically; fixed-size page cannot fit it')
|
||||
number = len(pages) + 1
|
||||
footer = fonts.text(str(number), style.width / 2, style.height - style.margin_y, 12, 'page-number', (), True)
|
||||
pages.append(Page(number, style.width, style.height, style.supersampling, tuple(placed), tuple(header + [p for s in placed for p in s.primitives] + [footer])))
|
||||
pages.append(generator.engraving.types.Page(number, style.width, style.height, style.supersampling, tuple(placed), tuple(header + [p for s in placed for p in s.primitives] + [footer])))
|
||||
placed = []
|
||||
header = page_header(len(pages) + 1)
|
||||
cursor = bounds(header).bottom + 24
|
||||
cursor = generator.engraving.types.bounds(header).bottom + 24
|
||||
if cursor + height > style.height - style.margin_y - 18:
|
||||
raise EngravingError(f'at musical offset {system.start}: simultaneous notation is too tall for a page')
|
||||
raise generator.engraving.types.EngravingError(f'at musical offset {system.start}: simultaneous notation is too tall for a page')
|
||||
dx, dy = style.margin_x + 1, cursor - system.box.top
|
||||
moved = replace(system, primitives=tuple(p.moved(dx, dy) for p in system.primitives), anchors=tuple(a.moved(dx, dy) for a in system.anchors), box=system.box.moved(dx, dy))
|
||||
moved = dataclasses.replace(system, primitives=tuple(p.moved(dx, dy) for p in system.primitives), anchors=tuple(a.moved(dx, dy) for a in system.anchors), box=system.box.moved(dx, dy))
|
||||
placed.append(moved)
|
||||
cursor += height + style.system_gap
|
||||
if placed:
|
||||
number = len(pages) + 1
|
||||
footer = fonts.text(str(number), style.width / 2, style.height - style.margin_y, 12, 'page-number', (), True)
|
||||
pages.append(Page(number, style.width, style.height, style.supersampling, tuple(placed), tuple(header + [p for s in placed for p in s.primitives] + [footer])))
|
||||
pages.append(generator.engraving.types.Page(number, style.width, style.height, style.supersampling, tuple(placed), tuple(header + [p for s in placed for p in s.primitives] + [footer])))
|
||||
return tuple(pages)
|
||||
|
||||
def layout_score(document: dict, style: Style, fonts: Fonts) -> LayoutDocument:
|
||||
validate_document(document)
|
||||
index = ScoreIndex(document)
|
||||
def layout_systems(document: dict, style: generator.engraving.types.Style, fonts: generator.engraving.fonts.Fonts) -> tuple[generator.engraving.types.System, ...]:
|
||||
generator.model.validate_document(document)
|
||||
index = generator.engraving.notation.ScoreIndex(document)
|
||||
if not index.measures:
|
||||
raise EngravingError('score has no measures')
|
||||
raise generator.engraving.types.EngravingError('score has no measures')
|
||||
systems = []
|
||||
start = Fraction()
|
||||
start = fractions.Fraction()
|
||||
available = style.width - style.margin_x * 2
|
||||
while start < index.end:
|
||||
candidates = index.boundary_times[bisect_right(index.boundary_times, start):]
|
||||
columns = collections.OrderedDict()
|
||||
candidates = index.boundary_times[bisect.bisect_right(index.boundary_times, start):]
|
||||
selected = None
|
||||
# Reserve the same title/credit and footer bands when considering a split,
|
||||
# so a wide pitch range can break vertically as well as horizontally.
|
||||
height_limit = style.height - style.margin_y * 2 - 110
|
||||
for end in candidates:
|
||||
trial = make_system(index, start, end, fonts, style)
|
||||
trial = make_system(index, start, end, fonts, style, columns=columns)
|
||||
if trial.box.right - trial.box.left > available or trial.box.bottom - trial.box.top > height_limit:
|
||||
break
|
||||
selected = trial
|
||||
if selected is None:
|
||||
measure_end = candidates[0]
|
||||
for end in index.column_times[bisect_right(index.column_times, start):bisect_left(index.column_times, measure_end)]:
|
||||
trial = make_system(index, start, end, fonts, style)
|
||||
for end in index.column_times[bisect.bisect_right(index.column_times, start):bisect.bisect_left(index.column_times, measure_end)]:
|
||||
trial = make_system(index, start, end, fonts, style, columns=columns)
|
||||
if trial.box.right - trial.box.left <= available and trial.box.bottom - trial.box.top <= height_limit:
|
||||
selected = trial
|
||||
else:
|
||||
break
|
||||
if selected is None:
|
||||
raise EngravingError(f'at musical offset {start}: irreducible simultaneous content exceeds the page width or height')
|
||||
systems.append(make_system(index, start, selected.end, fonts, style, justify=selected.end < index.end))
|
||||
raise generator.engraving.types.EngravingError(f'at musical offset {start}: irreducible simultaneous content exceeds the page width or height')
|
||||
systems.append(make_system(index, start, selected.end, fonts, style, justify=True, columns=columns) if selected.end < index.end else selected)
|
||||
start = selected.end
|
||||
layout = LayoutDocument(paginate(systems, document, fonts, style), style, fonts.fingerprint(style))
|
||||
from generator.engraving.audit import audit_layout
|
||||
audit_layout(document, layout, fonts)
|
||||
return tuple(systems)
|
||||
|
||||
def layout_score(document: dict, style: generator.engraving.types.Style, fonts: generator.engraving.fonts.Fonts) -> generator.engraving.types.LayoutDocument:
|
||||
systems = layout_systems(document, style, fonts)
|
||||
layout = generator.engraving.types.LayoutDocument(paginate(systems, document, fonts, style), style, fonts.fingerprint(style))
|
||||
generator.engraving.audit.audit_layout(document, layout, fonts)
|
||||
return layout
|
||||
|
||||
Reference in New Issue
Block a user