from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from sortedcontainers import SortedKeyList # type: ignore
from neoscore.core import neoscore
from neoscore.core.layout_controllers import NewLine
from neoscore.core.units import ZERO, Unit
from neoscore.western.staff_fringe_layout import StaffFringeLayout
if TYPE_CHECKING:
from neoscore.western.abstract_staff import AbstractStaff
[docs]class StaffGroup:
"""A collection of staves.
This provides a shared context for staff systems, useful in situations like aligning
staff fringes.
Each staff has a reference to a staff group, even if it's a single-item group. See
:obj:`.AbstractStaff.group`.
"""
# Fringe padding constants in pseudo-staff-units
RIGHT_PADDING = 1
"""Padding to the right of all staff fringes, in pseudo-staff-units."""
CLEF_LEFT_PADDING = 0.5
"""Padding to the left of clefs in fringes, in pseudo-staff-units."""
TIME_SIG_LEFT_PADDING = 0.5
"""Padding to the left of time signatures in fringes, in pseudo-staff-units."""
KEY_SIG_LEFT_PADDING = 0.25
"""Padding to the left of key signatures in fringes, in pseudo-staff-units."""
[docs] def __init__(self) -> None:
self._fringe_layout_cache: Dict[
Tuple[AbstractStaff, Optional[NewLine]], StaffFringeLayout
] = {}
# Sort staves according to position to some arbitrary known object
self._staves: SortedKeyList[AbstractStaff] = SortedKeyList(
key=lambda s: neoscore.document.pages[0].map_to(s).y
)
@property
def staves(self) -> SortedKeyList[AbstractStaff]:
"""The staves contained in this group.
Staves shouldn't be directly added to this list; instead register a staff with
the group when the staff is initialized.
This list is automatically sorted in visually descending order.
"""
return self._staves
[docs] def fringe_layout_at(
self, staff: AbstractStaff, location: Optional[NewLine]
) -> StaffFringeLayout:
"""Compute to fringe layout needed for a staff at a given location.
This automatically aligns the returned layout with the fringes of other staves
in the group.
Layouts are internally cached so this is fairly efficient.
"""
# If the layout is already cached, return it
cached_value = self._fringe_layout_cache.get((staff, location))
if cached_value:
return cached_value
# If the staff doesn't actually exist at the beginning of a line,
# Do its fringe layout in isolation
if not self._staff_exists_at(staff, location):
layout = staff.fringe_layout_for_isolated_staff(location)
self._fringe_layout_cache[(staff, location)] = layout
return layout
# Work out the layouts of each staff in isolation first
isolated_layouts = []
min_staff_basis = ZERO # Wider fringes give a lower number here (further left)
for iter_staff in self.staves:
if not self._staff_exists_at(iter_staff, location):
continue
layout = iter_staff.fringe_layout_for_isolated_staff(location)
min_staff_basis = min(min_staff_basis, layout.staff)
isolated_layouts.append((iter_staff, layout))
# Then align each layout to fit the widest layout found, and store each in cache
for iter_staff, isolated_layout in isolated_layouts:
aligned_layout = self._align_layout(isolated_layout, min_staff_basis)
self._fringe_layout_cache[(iter_staff, location)] = aligned_layout
# Now the requested layout is cached, so return it
return self._fringe_layout_cache[(staff, location)]
@staticmethod
def _staff_exists_at(staff: AbstractStaff, location: Optional[NewLine]) -> bool:
"""Check if a staff exists at a given location"""
if not location:
# No good way currently to check without a location
return True
staff_pos_x = staff.flowable.descendant_pos_x(staff)
return (staff_pos_x <= location.flowable_x) and (
staff_pos_x + staff.breakable_length >= location.flowable_x
)
@staticmethod
def _align_layout(
layout: StaffFringeLayout, staff_basis: Unit
) -> StaffFringeLayout:
if layout.staff == staff_basis:
return layout
# Find the delta from the layout's staff basis to the alignment's
# Note that these are all *negative* numbers
delta = staff_basis - layout.staff
return StaffFringeLayout(
layout.pos_x_in_staff,
# Staff, clef, and key signatures are left-aligned to the widest fringe
layout.staff + delta,
layout.clef + delta,
layout.key_signature + delta,
# Time signatures are right-aligned
layout.time_signature,
)