Generation
This commit is contained in:
@@ -24,6 +24,10 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
|
||||
fragments, primitives = {}, {}
|
||||
previous_end = fractions.Fraction()
|
||||
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)
|
||||
for item in page.primitives:
|
||||
for source in item.sources:
|
||||
@@ -36,7 +40,7 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
|
||||
for anchor in system.anchors:
|
||||
if anchor.fragment.kind == 'tone':
|
||||
expected = index.pitch_position(index.tones[anchor.source][1], anchor.time)
|
||||
if abs(anchor.y - (baseline - expected * layout.style.space / 2)) > 0.01:
|
||||
if abs(anchor.y - (baseline - expected * layout.style.space * system.scale / 2)) > 0.01:
|
||||
raise generator.engraving.types.EngravingError(f'{anchor.source}: displayed staff position changed pitch')
|
||||
role = 'head' if anchor.fragment.kind == 'tone' else anchor.fragment.kind
|
||||
candidates = [p for p in system.primitives if p.role == role and anchor.source in p.sources and abs(p.points[0][0] - anchor.x) < 0.01]
|
||||
@@ -51,7 +55,7 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
|
||||
tone = index.tones[anchor.source][1]
|
||||
tab = tone['tablature']
|
||||
tab_baseline = max(p.points[0][1] for p in system.primitives if p.role == 'tab-staff')
|
||||
expected_y = tab_baseline - (6 - tab['string']) * layout.style.space + 5
|
||||
expected_y = tab_baseline + (4 - (6 - tab['string']) * layout.style.tab_space) * system.scale
|
||||
expected_x = min(a.x for a in system.anchors if a.event == anchor.event and a.time == anchor.time)
|
||||
tabs = [p for p in system.primitives if p.role == 'tab' and anchor.source in p.sources]
|
||||
if len(tabs) != 1 or tabs[0].value != str(tab['fret']) or abs(tabs[0].points[0][1] - expected_y) > 0.01 or abs(tabs[0].points[0][0] - expected_x) > 0.01:
|
||||
@@ -67,7 +71,7 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
|
||||
continue
|
||||
if item.role != other.role and {item.role, other.role} - {'head', 'tab', 'accidental'}:
|
||||
continue
|
||||
if item.box.overlaps(other.box, 0.1):
|
||||
if item.box.overlaps(other.box, 0.1 * system.scale):
|
||||
raise generator.engraving.types.EngravingError(f'{item.sources} and {other.sources}: colliding {item.role}/{other.role}')
|
||||
if previous_end != index.end:
|
||||
raise generator.engraving.types.EngravingError('layout omits the end of the score')
|
||||
|
||||
@@ -53,14 +53,14 @@ def column(index: generator.engraving.notation.ScoreIndex, time: fractions.Fract
|
||||
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(generator.harmony.symbol(item), 0, 0, 21, 'harmony', (item['id'],)))
|
||||
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'):
|
||||
items.append(fonts.text(label, 0, 0, 19, 'section', (measure['id'] + ':section',)))
|
||||
items.append(fonts.text(label, 0, 0, 16 if index.staff.get('tablature') else 19, 'section', (measure['id'] + ':section',)))
|
||||
return items, anchors, fragments
|
||||
|
||||
def column_metrics(index, time, start, end, fonts, style, columns):
|
||||
@@ -157,14 +157,12 @@ def make_system(index: generator.engraving.notation.ScoreIndex, start: fractions
|
||||
|
||||
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 = []
|
||||
cursor = style.margin_y
|
||||
first = True
|
||||
guitar = document['staves'][0].get('tablature', False)
|
||||
header_gap = 14 if guitar else 24
|
||||
|
||||
def page_header(number):
|
||||
title = document['metadata']['title']
|
||||
size = 26 if number == 1 else 17
|
||||
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
|
||||
@@ -175,32 +173,23 @@ def paginate(systems: list[generator.engraving.types.System], document: dict, fo
|
||||
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 + 22, 16, '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
|
||||
|
||||
for system in systems:
|
||||
height = system.box.bottom - system.box.top
|
||||
if first:
|
||||
header = page_header(len(pages) + 1)
|
||||
cursor = generator.engraving.types.bounds(header).bottom + 24
|
||||
first = False
|
||||
if cursor + height > style.height - style.margin_y - 18:
|
||||
if not placed:
|
||||
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(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 = generator.engraving.types.bounds(header).bottom + 24
|
||||
if cursor + height > style.height - style.margin_y - 18:
|
||||
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 = 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:
|
||||
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 = systems[first:first + 2]
|
||||
scale = min(1, row_height / max(s.box.bottom - s.box.top for s in pair))
|
||||
placed = []
|
||||
for row, system in enumerate(pair):
|
||||
system = system.scaled(scale)
|
||||
dx, dy = style.margin_x + 1, top + row * (row_height + style.system_gap) - 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)
|
||||
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)
|
||||
@@ -240,6 +229,7 @@ def layout_systems(document: dict, style: generator.engraving.types.Style, fonts
|
||||
return tuple(systems)
|
||||
|
||||
def layout_score(document: dict, style: generator.engraving.types.Style, fonts: generator.engraving.fonts.Fonts) -> generator.engraving.types.LayoutDocument:
|
||||
style = generator.engraving.types.score_style(document, style)
|
||||
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)
|
||||
|
||||
@@ -39,7 +39,8 @@ def draw_event(index: generator.engraving.notation.ScoreIndex, event: generator.
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -120,6 +120,7 @@ def engrave_group(notation, paths, root, font_path, cache_dir, fingerprint, prog
|
||||
for path, score_key in paths:
|
||||
try:
|
||||
document = json.loads(path.read_text(encoding='utf-8'))
|
||||
style = generator.engraving.types.score_style(document, generator.engraving.types.Style())
|
||||
destination = path.with_suffix('.pages')
|
||||
clear_pages(destination)
|
||||
destination.mkdir()
|
||||
|
||||
@@ -11,14 +11,13 @@ def column(index, event, anchors, fonts, style):
|
||||
sources = (event.value['id'],)
|
||||
if not event.value['pitches']:
|
||||
anchor = anchors[0]
|
||||
glyph = 'noteheadSlashHorizontalEnds' if anchor.fragment.kind == 'slash' else generator.engraving.notes.rest_glyph(anchor.fragment.written_type)
|
||||
return [fonts.glyph(glyph, x, -2 * style.space, style.space, 'tab-' + anchor.fragment.kind, sources)]
|
||||
return [fonts.glyph('noteheadSlashHorizontalEnds', x, -2 * style.tab_space, style.space, 'tab-slash', sources)] if anchor.fragment.kind == 'slash' else []
|
||||
result = []
|
||||
for anchor in anchors:
|
||||
tone = index.tones[anchor.source][1]
|
||||
tab = tone['tablature']
|
||||
y = -(6 - tab['string']) * style.space
|
||||
result.append(fonts.text(str(tab['fret']), x, y + 5, style.space * 1.2, 'tab', (event.value['id'], tone['id'])))
|
||||
y = -(6 - tab['string']) * style.tab_space
|
||||
result.append(fonts.text(str(tab['fret']), x, y + 4, style.tab_font_size, 'tab', (event.value['id'], tone['id'])))
|
||||
return result
|
||||
|
||||
def staff(index, items, start, end, left, right, fonts, style):
|
||||
@@ -26,12 +25,12 @@ def staff(index, items, start, end, left, right, fonts, style):
|
||||
numbers = [p for p in items if p.role.startswith('tab')]
|
||||
if not index.staff.get('tablature'):
|
||||
return items
|
||||
offset = generator.engraving.types.bounds(notation).bottom + 32 + 5 * style.space
|
||||
offset = generator.engraving.types.bounds(notation).bottom + style.tab_gap + 5 * style.tab_space
|
||||
numbers = [p.moved(0, offset) for p in numbers]
|
||||
drawing = list(numbers)
|
||||
drawing.append(fonts.text('TAB', 0, offset - 2 * style.space, 15, 'tab-label', ()))
|
||||
drawing.append(fonts.text('TAB', 0, offset - 2 * style.tab_space, style.tab_font_size, 'tab-label', ()))
|
||||
for string in range(1, 7):
|
||||
y = offset - (6 - string) * style.space
|
||||
y = offset - (6 - string) * style.tab_space
|
||||
cuts = sorted((max(0, p.box.left - 2), min(right, p.box.right + 2)) for p in numbers if p.box.top <= y <= p.box.bottom)
|
||||
cursor = 0
|
||||
for a, b in cuts + [(right, right)]:
|
||||
@@ -39,7 +38,7 @@ def staff(index, items, start, end, left, right, fonts, style):
|
||||
drawing.append(generator.engraving.types.line('tab-staff', (), ((cursor, y), (a, y)), style.space * 0.13))
|
||||
cursor = max(cursor, b)
|
||||
for bar in (p for p in notation if p.role == 'barline'):
|
||||
drawing.append(generator.engraving.types.line('tab-barline', bar.sources, tuple((x, offset + y * 5 / 4) for x, y in bar.points), bar.thickness))
|
||||
drawing.append(generator.engraving.types.line('tab-barline', bar.sources, tuple((x, offset + y * 5 * style.tab_space / (4 * style.space)) for x, y in bar.points), bar.thickness))
|
||||
by_source = {source: p for p in numbers if p.role == 'tab' for source in p.sources[1:]}
|
||||
for identifier, (event, tone) in index.tones.items():
|
||||
target = tone.get('tie_to')
|
||||
@@ -53,7 +52,7 @@ def staff(index, items, start, end, left, right, fonts, style):
|
||||
first, last = by_source.get(identifier), by_source.get(target)
|
||||
x1 = first.box.right + 2 if first else left
|
||||
x2 = last.box.left - 2 if last else right
|
||||
y = offset - (6 - tone['tablature']['string']) * style.space + 8
|
||||
y = offset - (6 - tone['tablature']['string']) * style.tab_space + 6
|
||||
if x2 > x1:
|
||||
drawing.append(generator.engraving.types.curve('tab-tie', (identifier,), ((x1, y), (x1 + (x2 - x1) / 3, y + 5), (x2 - (x2 - x1) / 3, y + 5), (x2, y))))
|
||||
return notation + drawing
|
||||
|
||||
@@ -17,6 +17,13 @@ class Style:
|
||||
supersampling: int = 4
|
||||
column_gap: float = 10
|
||||
system_gap: float = 26
|
||||
guitar_space: float = 10
|
||||
tab_space: float = 10.5
|
||||
tab_gap: float = 12
|
||||
tab_font_size: float = 12
|
||||
|
||||
def score_style(document: dict, style: Style) -> Style:
|
||||
return dataclasses.replace(style, space=style.guitar_space, system_gap=14) if document['staves'][0].get('tablature') else style
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Box:
|
||||
@@ -26,6 +33,8 @@ class Box:
|
||||
bottom: float
|
||||
def moved(self, x: float, y: float) -> Box:
|
||||
return Box(self.left + x, self.top + y, self.right + x, self.bottom + y)
|
||||
def scaled(self, factor: float) -> Box:
|
||||
return Box(self.left * factor, self.top * factor, self.right * factor, self.bottom * factor)
|
||||
def overlaps(self, other: Box, clearance: float = 0) -> bool:
|
||||
return self.left < other.right + clearance and other.left < self.right + clearance and self.top < other.bottom + clearance and other.top < self.bottom + clearance
|
||||
def finite(self) -> bool:
|
||||
@@ -43,6 +52,8 @@ class Primitive:
|
||||
thickness: float = 0
|
||||
def moved(self, x: float, y: float) -> Primitive:
|
||||
return dataclasses.replace(self, box=self.box.moved(x, y), points=tuple((a + x, b + y) for a, b in self.points))
|
||||
def scaled(self, factor: float) -> Primitive:
|
||||
return dataclasses.replace(self, box=self.box.scaled(factor), points=tuple((x * factor, y * factor) for x, y in self.points), size=self.size * factor, thickness=self.thickness * factor)
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Fragment:
|
||||
@@ -75,6 +86,8 @@ class Anchor:
|
||||
fragment: Fragment
|
||||
def moved(self, x: float, y: float) -> Anchor:
|
||||
return dataclasses.replace(self, x=self.x + x, y=self.y + y, stem_x=self.stem_x + x, stem_y=self.stem_y + y)
|
||||
def scaled(self, factor: float) -> Anchor:
|
||||
return dataclasses.replace(self, x=self.x * factor, y=self.y * factor, stem_x=self.stem_x * factor, stem_y=self.stem_y * factor)
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class System:
|
||||
@@ -84,6 +97,9 @@ class System:
|
||||
anchors: tuple[Anchor, ...]
|
||||
fragments: tuple[Fragment, ...]
|
||||
box: Box
|
||||
scale: float = 1
|
||||
def scaled(self, factor: float) -> System:
|
||||
return dataclasses.replace(self, primitives=tuple(p.scaled(factor) for p in self.primitives), anchors=tuple(a.scaled(factor) for a in self.anchors), box=self.box.scaled(factor), scale=self.scale * factor)
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Page:
|
||||
|
||||
Reference in New Issue
Block a user