Generation

This commit is contained in:
khodowany
2026-09-30 01:38:16 -05:00
parent b1a7b44d7a
commit deaa0468a7
20 changed files with 417 additions and 198 deletions
+35 -24
View File
@@ -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