Generation
This commit is contained in:
@@ -33,6 +33,13 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
|
||||
for source in item.sources:
|
||||
primitives.setdefault(source, []).append(item)
|
||||
for system in page.systems:
|
||||
for role in ('staff', 'tab-staff'):
|
||||
lines = [p for p in system.primitives if p.role == role]
|
||||
if lines:
|
||||
left = min(x for p in lines for x, _ in p.points)
|
||||
right = max(x for p in lines for x, _ in p.points)
|
||||
if abs(left - layout.style.margin_x - 1) > 0.01 or abs(right - (page.width - layout.style.margin_x - 1)) > 0.01:
|
||||
raise generator.engraving.types.EngravingError('music systems must have consistent rendered staff widths')
|
||||
if system.start != previous_end or system.end <= system.start:
|
||||
raise generator.engraving.types.EngravingError('layout systems do not partition the score timeline')
|
||||
previous_end = system.end
|
||||
|
||||
@@ -78,7 +78,7 @@ def column_metrics(index, time, start, end, fonts, style, columns):
|
||||
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:
|
||||
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, staff_width: float | None = None) -> generator.engraving.types.System:
|
||||
items, anchors, fragments = [], [], []
|
||||
state = index.state(start)
|
||||
measure = index.measure_at(start)[2]
|
||||
@@ -118,7 +118,7 @@ def make_system(index: generator.engraving.notation.ScoreIndex, start: fractions
|
||||
cursor = x + max(style.space * 1.4, box.right) + spring
|
||||
groups.append((item_start, len(items), anchor_start, len(anchors)))
|
||||
width = cursor + 6
|
||||
available = style.width - style.margin_x * 2
|
||||
available = staff_width + 1 if staff_width is not None else style.width - style.margin_x * 2
|
||||
if justify and width < available:
|
||||
stretch = (available - width) / max(1, len(times))
|
||||
for i, (first_item, last_item, first_anchor, last_anchor) in enumerate(groups):
|
||||
@@ -126,6 +126,10 @@ def make_system(index: generator.engraving.notation.ScoreIndex, start: fractions
|
||||
anchors[first_anchor:last_anchor] = [a.moved(i * stretch, 0) for a in anchors[first_anchor:last_anchor]]
|
||||
placements = [(time, x + i * stretch) for i, (time, x) in enumerate(placements)]
|
||||
width = available
|
||||
if staff_width is not None:
|
||||
if width > available + 0.01:
|
||||
raise generator.engraving.types.EngravingError(f'at musical offset {start}: content exceeds the final staff width')
|
||||
width = available
|
||||
right = width - 5
|
||||
last_measure = index.measure_at(end, before=True)[2]
|
||||
tab_items = [p for p in items if p.role.startswith('tab')]
|
||||
@@ -155,8 +159,22 @@ 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 paginate(systems: list[generator.engraving.types.System], document: dict, fonts: generator.engraving.fonts.Fonts, style: generator.engraving.types.Style) -> tuple[generator.engraving.types.Page, ...]:
|
||||
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))
|
||||
columns = collections.OrderedDict()
|
||||
# Re-space columns at the final physical width; scale glyphs uniformly, not horizontally.
|
||||
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:
|
||||
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')
|
||||
|
||||
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
|
||||
|
||||
@@ -183,31 +201,29 @@ def paginate(systems: list[generator.engraving.types.System], document: dict, fo
|
||||
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))
|
||||
pair = fit_page_systems(systems[first:first + 2], index, row_height, fonts, style)
|
||||
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)
|
||||
|
||||
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)
|
||||
def layout_systems(document: dict, style: generator.engraving.types.Style, fonts: generator.engraving.fonts.Fonts, *, index=None) -> tuple[generator.engraving.types.System, ...]:
|
||||
if index is None:
|
||||
generator.model.validate_document(document)
|
||||
index = generator.engraving.notation.ScoreIndex(document)
|
||||
if not index.measures:
|
||||
raise generator.engraving.types.EngravingError('score has no measures')
|
||||
systems = []
|
||||
start = fractions.Fraction()
|
||||
available = style.width - style.margin_x * 2
|
||||
available = style.width - style.margin_x * 2 - 2
|
||||
while start < index.end:
|
||||
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.
|
||||
# Reserve title/credit and footer bands before fitting complete measures.
|
||||
height_limit = style.height - style.margin_y * 2 - 110
|
||||
for end in candidates:
|
||||
trial = make_system(index, start, end, fonts, style, columns=columns)
|
||||
@@ -215,22 +231,17 @@ def layout_systems(document: dict, style: generator.engraving.types.Style, fonts
|
||||
break
|
||||
selected = trial
|
||||
if selected is None:
|
||||
measure_end = candidates[0]
|
||||
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 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)
|
||||
measure = index.measure_at(start)[2]
|
||||
raise generator.engraving.types.EngravingError(f'measure {measure["number"]} ({measure["id"]}): complete measure exceeds the page width or height at readable spacing')
|
||||
systems.append(selected)
|
||||
start = selected.end
|
||||
return tuple(systems)
|
||||
|
||||
def layout_score(document: dict, style: generator.engraving.types.Style, fonts: generator.engraving.fonts.Fonts) -> generator.engraving.types.LayoutDocument:
|
||||
generator.model.validate_document(document)
|
||||
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))
|
||||
index = generator.engraving.notation.ScoreIndex(document)
|
||||
systems = layout_systems(document, style, fonts, index=index)
|
||||
layout = generator.engraving.types.LayoutDocument(paginate(systems, document, fonts, style, index=index), style, fonts.fingerprint(style))
|
||||
generator.engraving.audit.audit_layout(document, layout, fonts)
|
||||
return layout
|
||||
|
||||
@@ -60,6 +60,7 @@ class ScoreIndex:
|
||||
time = end
|
||||
self.end = time
|
||||
self.measure_starts = [item[0] for item in self.measures]
|
||||
self.measure_starts_by_id = {measure['id']: start for start, _, measure in self.measures}
|
||||
self.events.sort(key=lambda e: (e.start, not bool(e.value.get('grace')), e.lane))
|
||||
self.event_starts = []
|
||||
self.event_max_ends = []
|
||||
|
||||
@@ -17,6 +17,7 @@ import generator.engraving.audit
|
||||
import generator.engraving.cache
|
||||
import generator.engraving.fonts
|
||||
import generator.engraving.layout
|
||||
import generator.engraving.notation
|
||||
import generator.engraving.raster
|
||||
import generator.engraving.types
|
||||
import generator.engraving.validation
|
||||
@@ -111,6 +112,7 @@ def engrave_group(notation, paths, root, font_path, cache_dir, fingerprint, prog
|
||||
family = generator.engraving.fonts.Fonts(font_path)
|
||||
style = generator.engraving.types.Style()
|
||||
systems = None
|
||||
index = None
|
||||
scores = {}
|
||||
audited = set()
|
||||
result = GroupResult()
|
||||
@@ -132,10 +134,12 @@ def engrave_group(notation, paths, root, font_path, cache_dir, fingerprint, prog
|
||||
else:
|
||||
if systems is None:
|
||||
started = time.monotonic()
|
||||
systems = generator.engraving.layout.layout_systems(document, style, family)
|
||||
generator.model.validate_document(document)
|
||||
index = generator.engraving.notation.ScoreIndex(document)
|
||||
systems = generator.engraving.layout.layout_systems(document, style, family, index=index)
|
||||
result.timings['layout'] += time.monotonic() - started
|
||||
started = time.monotonic()
|
||||
pages_layout = generator.engraving.layout.paginate(systems, document, family, style)
|
||||
pages_layout = generator.engraving.layout.paginate(systems, document, family, style, index=index)
|
||||
placement = tuple((page.width, page.height, tuple((system.start, system.end, system.box) for system in page.systems)) for page in pages_layout)
|
||||
if placement not in audited:
|
||||
layout = generator.engraving.types.LayoutDocument(pages_layout, style, fingerprint)
|
||||
|
||||
@@ -17,7 +17,7 @@ def beam_groups(index: generator.engraving.notation.ScoreIndex) -> tuple[tuple[s
|
||||
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 = next(start for start, _, measure in index.measures if measure['id'] == event.measure['id'])
|
||||
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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import generator.engraving.notes
|
||||
import generator.engraving.types
|
||||
import generator.model
|
||||
|
||||
@@ -40,19 +39,24 @@ def staff(index, items, start, end, left, right, fonts, style):
|
||||
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 * 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')
|
||||
finish = event.start + generator.model.fraction(tone['duration'])
|
||||
if not target and not (event.start < start < finish or event.start < end < finish):
|
||||
continue
|
||||
if target:
|
||||
finish = index.tones[target][0].start
|
||||
if finish < start or event.start >= end:
|
||||
continue
|
||||
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
|
||||
|
||||
def tie(identifier, tone, x1, x2):
|
||||
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))))
|
||||
|
||||
for identifier, (event, tone) in index.tones.items():
|
||||
first = by_source.get(identifier)
|
||||
finish = event.start + generator.model.fraction(tone['duration'])
|
||||
if first and event.start < start < finish:
|
||||
tie(identifier, tone, left, first.box.left - 2)
|
||||
if first and finish > end:
|
||||
tie(identifier, tone, first.box.right + 2, right)
|
||||
elif target := tone.get('tie_to'):
|
||||
destination = index.tones[target][0].start
|
||||
if event.start >= end or destination < start:
|
||||
continue
|
||||
last = by_source.get(target)
|
||||
if first or last:
|
||||
tie(identifier, tone, first.box.right + 2 if first else left, last.box.left - 2 if last else right)
|
||||
return notation + drawing
|
||||
|
||||
Reference in New Issue
Block a user