@@ -101,22 +101,3 @@ def barline(style: str, x: float, top: float, source: str) -> list[generator.eng
|
||||
return [generator.engraving.types.line('barline', references, ((x, top - 4 if style == 'tick' else top * 0.75), (x, top + 5 if style == 'tick' else top * 0.25)))]
|
||||
weights = {'regular': [1.4], 'heavy': [4], 'light-light': [1.4, 1.4], 'light-heavy': [1.4, 4], 'heavy-light': [4, 1.4], 'heavy-heavy': [4, 4]}[style]
|
||||
return [generator.engraving.types.line('barline', references, ((x - (len(weights) - 1 - i) * 5, top), (x - (len(weights) - 1 - i) * 5, 0)), width) for i, width in enumerate(weights)]
|
||||
|
||||
def tempo(value: dict, fonts: generator.engraving.fonts.Fonts, x: float, y: float, space: float, source: str) -> list[generator.engraving.types.Primitive]:
|
||||
written = value['written']
|
||||
references = (source,)
|
||||
glyph = fonts.glyph(generator.engraving.notation.head_glyph(written, None), x, y, space * 0.7, 'tempo', references)
|
||||
items = [glyph]
|
||||
stem_x = glyph.box.right - 0.5
|
||||
if written['type'] not in ('whole', 'breve', 'long', 'maxima'):
|
||||
items.append(generator.engraving.types.line('tempo', references, ((stem_x, y), (stem_x, y - 23)), 1))
|
||||
if generator.engraving.notation.flags(written):
|
||||
name = 'flag' + ('8th' if written['type'] == 'eighth' else written['type']) + 'Up'
|
||||
items.append(fonts.glyph(name, stem_x, y - 23, space * 0.7, 'tempo', references))
|
||||
cursor = max(p.box.right for p in items) + 3
|
||||
for _ in range(written.get('dots', 0)):
|
||||
dot = fonts.glyph('augmentationDot', cursor, y - 3, space * 0.7, 'tempo', references)
|
||||
items.append(dot)
|
||||
cursor = dot.box.right + 3
|
||||
items.append(fonts.text(' = ' + str(generator.model.fraction(value['bpm'])), cursor, y + 3, 16, 'tempo', references))
|
||||
return items
|
||||
|
||||
@@ -23,16 +23,23 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
|
||||
index = generator.engraving.notation.ScoreIndex(document)
|
||||
fragments, primitives = {}, {}
|
||||
previous_end = fractions.Fraction()
|
||||
scale = layout.pages[0].systems[0].scale
|
||||
for page in layout.pages:
|
||||
if len(page.systems) != 2 and (page is not layout.pages[-1] or len(page.systems) != 1):
|
||||
raise generator.engraving.types.EngravingError('pages must contain two music systems, except an odd final system')
|
||||
if len(page.systems) == 2 and page.systems[0].box.bottom + layout.style.system_gap > page.systems[1].box.top + 0.01:
|
||||
raise generator.engraving.types.EngravingError('music systems overlap their page rows')
|
||||
audit_primitives(page.primitives, page.width, page.height, fonts)
|
||||
header = generator.engraving.types.bounds([p for p in page.primitives if p.role in ('title', 'subtitle')])
|
||||
footer = generator.engraving.types.bounds([p for p in page.primitives if p.role == 'page-number'])
|
||||
if page.systems[0].box.top < header.bottom + layout.style.page_clearance - 0.01 or page.systems[-1].box.bottom > footer.top - layout.style.page_clearance + 0.01:
|
||||
raise generator.engraving.types.EngravingError('music overlaps the page header or footer')
|
||||
for item in page.primitives:
|
||||
for source in item.sources:
|
||||
primitives.setdefault(source, []).append(item)
|
||||
for system in page.systems:
|
||||
if system.scale != scale:
|
||||
raise generator.engraving.types.EngravingError('notation size must remain consistent throughout a part')
|
||||
for role in ('staff', 'tab-staff'):
|
||||
lines = [p for p in system.primitives if p.role == role]
|
||||
if lines:
|
||||
@@ -123,9 +130,6 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
|
||||
label = generator.harmony.symbol(value)
|
||||
if not any(p.role == 'harmony' and p.value == label for p in primitives.get(value['id'], [])):
|
||||
raise generator.engraving.types.EngravingError(f'{value["id"]}: harmony was omitted')
|
||||
for i, _ in enumerate(measure.get('tempos', [])):
|
||||
if not any(p.role == 'tempo' for p in primitives.get(measure['id'] + f':tempo:{i}', [])):
|
||||
raise generator.engraving.types.EngravingError(f'{measure["id"]}: tempo was omitted')
|
||||
for span in index.staff.get('tuplets', []):
|
||||
if span.get('show_number', 'actual') != 'none' or span.get('bracket', 'auto') != 'no':
|
||||
if not any(p.role.startswith('tuplet-') for p in primitives.get(span['id'], [])):
|
||||
|
||||
@@ -54,9 +54,6 @@ def column(index: generator.engraving.notation.ScoreIndex, time: fractions.Fract
|
||||
for owner, _, item in index.harmonies_by_onset.get(time, []):
|
||||
if owner is measure:
|
||||
items.append(fonts.text(generator.harmony.symbol(item), 0, 0, 18 if index.staff.get('tablature') else 21, 'harmony', (item['id'],)))
|
||||
for owner, i, value in index.tempos_by_onset.get(time, []):
|
||||
if owner is measure:
|
||||
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'):
|
||||
@@ -159,54 +156,84 @@ def make_system(index: generator.engraving.notation.ScoreIndex, start: fractions
|
||||
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 fit_page_systems(systems, index, row_height, fonts, style):
|
||||
scale = min(1, row_height / max(s.box.bottom - s.box.top for s in systems))
|
||||
def fit_part_systems(systems, index, available_height, fonts, style):
|
||||
|
||||
def fitting_scale(items):
|
||||
scale = 1
|
||||
for first in range(0, len(items), 2):
|
||||
pair = items[first:first + 2]
|
||||
height = sum(s.box.bottom - s.box.top for s in pair)
|
||||
scale = min(scale, (available_height - style.system_gap * (len(pair) - 1)) / height)
|
||||
return scale
|
||||
|
||||
scale = fitting_scale(systems)
|
||||
columns = collections.OrderedDict()
|
||||
# Re-space columns at the final physical width; scale glyphs uniformly, not horizontally.
|
||||
# Fit the entire part together; only spacing, never page-specific glyph sizes, varies.
|
||||
for _ in range(8):
|
||||
width = (style.width - 2 * style.margin_x - 2) / scale
|
||||
fitted = [make_system(index, s.start, s.end, fonts, style, justify=s.end < index.end, columns=columns, staff_width=width) for s in systems]
|
||||
height = max(s.box.bottom - s.box.top for s in fitted)
|
||||
if height * scale <= row_height + 0.001:
|
||||
next_scale = fitting_scale(fitted)
|
||||
if scale <= next_scale + 0.000001:
|
||||
return [s.scaled(scale) for s in fitted]
|
||||
scale = row_height / height
|
||||
raise generator.engraving.types.EngravingError(f'at musical offset {systems[0].start}: page spacing did not converge')
|
||||
scale = next_scale
|
||||
raise generator.engraving.types.EngravingError('part spacing did not converge')
|
||||
|
||||
def part_header(document, fonts, style):
|
||||
size = 16
|
||||
title = document['metadata']['title']
|
||||
item = fonts.text(title, style.margin_x, style.margin_y + size, size, 'title', ())
|
||||
while item.box.right > style.width - style.margin_x and size > 12:
|
||||
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 generator.engraving.types.EngravingError('score title cannot fit the page without clipping')
|
||||
result = [item]
|
||||
subtitle = ' - '.join(str(value) for value in (document['metadata'].get('composer'), document['staves'][0].get('name')) if value)
|
||||
if not subtitle:
|
||||
return result
|
||||
credit = fonts.text(subtitle, 0, style.margin_y + size, 12, 'subtitle', ())
|
||||
width = credit.box.right - credit.box.left
|
||||
if item.box.right + 20 + width <= style.width - style.margin_x:
|
||||
result.append(credit.moved(style.width - style.margin_x - credit.box.right, 0))
|
||||
return result
|
||||
line = ''
|
||||
baseline = item.box.bottom + 15
|
||||
for word in subtitle.split():
|
||||
text = ' '.join(filter(None, (line, word)))
|
||||
candidate = fonts.text(text, style.margin_x, baseline, 12, 'subtitle', ())
|
||||
if candidate.box.right > style.width - style.margin_x and line:
|
||||
result.append(fonts.text(line, style.margin_x, baseline, 12, 'subtitle', ()))
|
||||
baseline = result[-1].box.bottom + 15
|
||||
text = word
|
||||
candidate = fonts.text(text, style.margin_x, baseline, 12, 'subtitle', ())
|
||||
if candidate.box.right > style.width - style.margin_x:
|
||||
raise generator.engraving.types.EngravingError('score credits cannot fit the page without clipping')
|
||||
line = text
|
||||
result.append(fonts.text(line, style.margin_x, baseline, 12, 'subtitle', ()))
|
||||
return result
|
||||
|
||||
def paginate(systems: list[generator.engraving.types.System], document: dict, fonts: generator.engraving.fonts.Fonts, style: generator.engraving.types.Style, *, index=None) -> tuple[generator.engraving.types.Page, ...]:
|
||||
pages = []
|
||||
index = index or generator.engraving.notation.ScoreIndex(document)
|
||||
guitar = document['staves'][0].get('tablature', False)
|
||||
header_gap = 14 if guitar else 24
|
||||
|
||||
def page_header(number):
|
||||
title = document['metadata']['title']
|
||||
size = (20 if number == 1 else 16) if guitar else (26 if number == 1 else 17)
|
||||
item = fonts.text(title, style.margin_x, style.margin_y + size, size, 'title', ())
|
||||
while item.box.right > style.width - style.margin_x and size > 12:
|
||||
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 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')) if value)
|
||||
if subtitle:
|
||||
result.append(fonts.text(subtitle, style.margin_x, item.box.bottom + (15 if guitar else 22), 12 if guitar else 16, 'subtitle', ()))
|
||||
return result
|
||||
|
||||
header = part_header(document, fonts, style)
|
||||
top = generator.engraving.types.bounds(header).bottom + style.page_clearance
|
||||
footers = [fonts.text(str(number), style.width / 2, style.height - style.margin_y, 12, 'page-number', (), True) for number in range(1, (len(systems) + 1) // 2 + 1)]
|
||||
bottom = min(p.box.top for p in footers) - style.page_clearance
|
||||
if bottom - top <= style.system_gap:
|
||||
raise generator.engraving.types.EngravingError('page header leaves no room for two music systems')
|
||||
fitted = fit_part_systems(systems, index, bottom - top, fonts, style)
|
||||
for first in range(0, len(systems), 2):
|
||||
number = len(pages) + 1
|
||||
header = page_header(number)
|
||||
top = generator.engraving.types.bounds(header).bottom + header_gap
|
||||
row_height = (style.height - style.margin_y - 24 - top - style.system_gap) / 2
|
||||
if row_height <= 0:
|
||||
raise generator.engraving.types.EngravingError('page header leaves no room for two music systems')
|
||||
pair = fit_page_systems(systems[first:first + 2], index, row_height, fonts, style)
|
||||
pair = fitted[first:first + 2]
|
||||
slack = max(0, bottom - top - sum(s.box.bottom - s.box.top for s in pair) - style.system_gap * (len(pair) - 1))
|
||||
spacing = slack / 3 if len(pair) == 2 else 0
|
||||
cursor = top + spacing
|
||||
placed = []
|
||||
for row, system in enumerate(pair):
|
||||
dx, dy = style.margin_x + 1, top + row * (row_height + style.system_gap) - system.box.top
|
||||
for system in pair:
|
||||
dx, dy = style.margin_x + 1, cursor - system.box.top
|
||||
placed.append(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)))
|
||||
footer = fonts.text(str(number), style.width / 2, style.height - style.margin_y, 12, 'page-number', (), True)
|
||||
cursor = placed[-1].box.bottom + style.system_gap + spacing
|
||||
footer = footers[first // 2]
|
||||
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)
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ class ScoreIndex:
|
||||
self.events_by_onset = {}
|
||||
self.attributes_by_onset = {}
|
||||
self.harmonies_by_onset = {}
|
||||
self.tempos_by_onset = {}
|
||||
self.by_id = {}
|
||||
self.tones = {}
|
||||
self.boundaries = {fractions.Fraction()}
|
||||
@@ -51,8 +50,7 @@ class ScoreIndex:
|
||||
self.columns.add(onset)
|
||||
for tone in note['pitches']:
|
||||
self.tones[tone['id']] = (event, tone)
|
||||
for field in ('attributes', 'harmonies', 'tempos'):
|
||||
bucket = {'attributes': self.attributes_by_onset, 'harmonies': self.harmonies_by_onset, 'tempos': self.tempos_by_onset}[field]
|
||||
for field, bucket in (('attributes', self.attributes_by_onset), ('harmonies', self.harmonies_by_onset)):
|
||||
for index, item in enumerate(measure.get(field, [])):
|
||||
onset = time + generator.model.fraction(item['onset'])
|
||||
self.columns.add(onset)
|
||||
|
||||
@@ -203,11 +203,11 @@ def draw_spans(index: generator.engraving.notation.ScoreIndex, anchors: list[gen
|
||||
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', 'tempo')]
|
||||
music = [p for p in items if p.role not in ('harmony', 'lyric', 'section')]
|
||||
box = generator.engraving.types.bounds(music)
|
||||
result = list(music)
|
||||
top = box.top - 12
|
||||
for role in ('harmony', 'tempo', 'section'):
|
||||
for role in ('harmony', 'section'):
|
||||
lane = [p for p in items if p.role == role]
|
||||
if lane:
|
||||
extent = generator.engraving.types.bounds(lane)
|
||||
|
||||
@@ -14,6 +14,7 @@ class Style:
|
||||
space: float = 12
|
||||
margin_x: float = 28
|
||||
margin_y: float = 18
|
||||
page_clearance: float = 14
|
||||
supersampling: int = 4
|
||||
column_gap: float = 10
|
||||
system_gap: float = 26
|
||||
|
||||
Reference in New Issue
Block a user