Scale Patterns
ci / checks (push) Successful in 48m15s

This commit is contained in:
khodowany
2026-09-30 03:17:06 -05:00
parent 7cad6db980
commit f5f0ab5eec
31 changed files with 626 additions and 32 deletions
+6 -2
View File
@@ -78,15 +78,19 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
fragments.setdefault(fragment.source, []).append(fragment)
# Staff, ledger, stem, beam and tie contacts are intentional. Distinct
# noteheads, accidentals and text in the same lane must remain separate.
checked = [p for p in system.primitives if p.role in ('head', 'tab', 'accidental', 'harmony', 'lyric')]
checked = [p for p in system.primitives if p.role in ('head', 'tab', 'accidental', 'harmony', 'lyric', 'scale', 'section')]
for i, item in enumerate(checked):
for other in checked[i + 1:]:
if set(item.sources) & set(other.sources) and (item.role != 'tab' or other.role != 'tab'):
continue
if item.role != other.role and {item.role, other.role} - {'head', 'tab', 'accidental'}:
if item.role != other.role and 'scale' not in (item.role, other.role) and {item.role, other.role} - {'head', 'tab', 'accidental'}:
continue
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}')
expected_scales = [span for span in index.scale_spans if generator.model.fraction(span['start']) < system.end and generator.model.fraction(span['end']) > system.start]
drawn_scales = [(p.sources[0], p.value) for p in system.primitives if p.role == 'scale']
if drawn_scales != [(span['id'], span['label']) for span in expected_scales]:
raise generator.engraving.types.EngravingError('solo scale labels are missing, duplicated, or changed')
if previous_end != index.end:
raise generator.engraving.types.EngravingError('layout omits the end of the score')
for event in index.events:
+2
View File
@@ -11,6 +11,7 @@ import generator.engraving.audit
import generator.engraving.fonts
import generator.engraving.notation
import generator.engraving.notes
import generator.engraving.scales
import generator.engraving.spans
import generator.engraving.tablature
import generator.engraving.types
@@ -54,6 +55,7 @@ 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'],)))
items.extend(generator.engraving.scales.column(index, time, start, fonts))
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'):
+3
View File
@@ -57,6 +57,9 @@ class ScoreIndex:
bucket.setdefault(onset, []).append((measure, index, item))
time = end
self.end = time
self.scale_spans = self.staff.get('scale_spans', [])
self.scale_starts = [generator.model.fraction(span['start']) for span in self.scale_spans]
self.columns.update(self.scale_starts)
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))
+14
View File
@@ -0,0 +1,14 @@
from __future__ import annotations
import bisect
import generator.model
def column(index, time, system_start, fonts):
position = bisect.bisect_right(index.scale_starts, time) - 1
if position < 0:
return []
span = index.scale_spans[position]
if time >= generator.model.fraction(span['end']) or time not in (system_start, index.scale_starts[position]):
return []
return [fonts.text(span['label'], 0, 0, 16, 'scale', (span['id'],))]
+3 -3
View File
@@ -123,7 +123,7 @@ def draw_spans(index: generator.engraving.notation.ScoreIndex, anchors: list[gen
y = anchor.y - anchor.direction * 8
if anchor.x - 2 > left:
result.append(generator.engraving.types.curve('tie', (identifier,), ((left, y), (left + 5, y - anchor.direction * 9), (anchor.x - 7, y - anchor.direction * 9), (anchor.x - 2, y)), 1.6))
top = min(-style.space * 5, min((p.box.top for p in items if p.role not in ('harmony', 'lyric', 'section', 'tempo')), default=0))
top = min(-style.space * 5, min((p.box.top for p in items if p.role not in ('harmony', 'lyric', 'section', 'scale', 'tempo')), default=0))
tuplet_lanes = []
for span in index.staff.get('tuplets', []):
a, b = index.by_id[span['start']], index.by_id[span['end']]
@@ -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')]
music = [p for p in items if p.role not in ('harmony', 'lyric', 'section', 'scale')]
box = generator.engraving.types.bounds(music)
result = list(music)
top = box.top - 12
for role in ('harmony', 'section'):
for role in ('harmony', 'scale', 'section'):
lane = [p for p in items if p.role == role]
if lane:
extent = generator.engraving.types.bounds(lane)