from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Type, cast
from neoscore.core.exceptions import NoClefError
from neoscore.core.layout_controllers import MarginController, NewLine
from neoscore.core.music_font import MusicFont
from neoscore.core.pen import Pen, PenDef
from neoscore.core.point import PointDef
from neoscore.core.positioned_object import PositionedObject, render_cached_property
from neoscore.core.units import ZERO, Mm, Unit, make_unit_class
from neoscore.western.abstract_staff import AbstractStaff
from neoscore.western.staff_fringe_layout import StaffFringeLayout
from neoscore.western.staff_group import StaffGroup
if TYPE_CHECKING:
from neoscore.western.clef import Clef
from neoscore.western.key_signature import KeySignature
[docs]class Staff(AbstractStaff):
"""A staff with some high-level knowledge of its contents."""
[docs] def __init__(
self,
pos: PointDef,
parent: Optional[PositionedObject],
length: Unit,
group: Optional[StaffGroup] = None,
line_spacing: Unit = Mm(1.75),
line_count: int = 5,
music_font_family: str = "Bravura",
pen: Optional[PenDef] = None,
):
"""
Args:
pos: The position of the top-left corner of the staff
parent: The parent for the staff. Make this a :obj:`.Flowable`
to allow the staff to run across line and page breaks.
length: The horizontal width of the staff
group: The staff group this belongs to. Set this if being used in a system
of multiple staves.
line_spacing: The distance between two lines in the staff.
line_count: The number of lines in the staff.
music_font_family: The name of the font to use for MusicText objects
in the staff. This defaults to the system-wide default music font
family.
pen: The pen used to draw the staff lines. Defaults to a line with
thickness from the music font's engraving default.
"""
unit = self._make_unit_class(line_spacing)
music_font = MusicFont(music_font_family, unit)
pen = pen or Pen(thickness=music_font.engraving_defaults["staffLineThickness"])
super().__init__(
pos, parent, length, group, unit(1), line_count, music_font, pen
)
[docs] def distance_to_next_of_type(self, staff_object: PositionedObject) -> Unit:
"""Find the x distance until the next occurrence of an object's type.
If the object is the last of its type, this gives the remaining length of the
staff after the object.
This is useful for determining rendering behavior of staff objects which are
active until another of their type occurs, such as :obj:`.KeySignature` and
:obj:`.Clef`.
"""
start_x = self.map_x_to(cast(PositionedObject, staff_object))
all_others_of_class = (
item
for item in self.descendants_of_exact_class(type(staff_object))
if item != staff_object
)
closest_x = Unit(float("inf"))
for item in all_others_of_class:
relative_x = self.map_x_to(item)
if start_x < relative_x < closest_x:
closest_x = relative_x
if closest_x == Unit(float("inf")):
return self.breakable_length - start_x
return closest_x - start_x
@render_cached_property
def clefs(self) -> List[Tuple[Unit, Clef]]:
"""All the clefs in this staff, ordered by their relative x pos."""
return self.find_ordered_descendants_with_attr("middle_c_staff_position")
[docs] def active_clef_at(self, pos_x: Unit) -> Optional[Clef]:
"""Return the active clef at a given x position, if any."""
return next(
(clef for (clef_x, clef) in reversed(self.clefs) if clef_x <= pos_x),
None,
)
@render_cached_property
def key_signatures(self) -> List[Tuple[Unit, KeySignature]]:
"""All the key signatures in this staff, ordered by their relative x pos."""
return self.find_ordered_descendants_with_attr(
"_neoscore_key_signature_type_marker"
)
@render_cached_property
def time_signatures(self) -> List[Tuple[Unit, KeySignature]]:
"""All the time signatures in this staff, ordered by their relative x pos."""
return self.find_ordered_descendants_with_attr(
"_neoscore_time_signature_type_marker"
)
[docs] def active_key_signature_at(self, pos_x: Unit) -> Optional[KeySignature]:
"""Return the active key signature at a given x position, if any."""
return next(
(sig for (sig_x, sig) in reversed(self.key_signatures) if sig_x <= pos_x),
None,
)
[docs] def middle_c_at(self, pos_x: Unit) -> Unit:
"""Find the y-axis staff position of middle-c at a given point.
Looks for clefs and other transposing modifiers to determine
the position of middle-c.
Raises:
NoClefError:
If no clef is active at the given position.
"""
clef = self.active_clef_at(pos_x)
if clef is None:
raise NoClefError
return clef.middle_c_staff_position
[docs] def y_on_ledger(self, pos_y: Unit) -> bool:
"""Determine if a y-axis position is approximately at a ledger line position
This is true for any whole-number staff position outside the staff
"""
return (not self.y_inside_staff(pos_y)) and self.unit(
pos_y
).display_value % 1 == 0
[docs] def ledgers_needed_for_y(self, position: Unit) -> List[Unit]:
"""Find the y positions of all ledgers needed for a given y position"""
# Work on positions as integers for simplicity
start = int(self.unit(position).display_value)
if start < 0:
return [self.unit(pos) for pos in range(start, 0, 1)]
elif start > self.line_count - 1:
return [self.unit(pos) for pos in range(start, self.line_count - 1, -1)]
else:
return []
@staticmethod
def _make_unit_class(staff_unit_size: Unit) -> Type[Unit]:
"""Create a Unit class with a ratio of 1 to a staff unit size."""
return make_unit_class("StaffUnit", staff_unit_size.base_value)
[docs] def register_layout_controllers(self):
# This is known to have some limitations in some cases when staves in a group
# have different key signatures. See issue #28.
flowable = self.flowable
if not flowable:
return
staff_flowable_x = flowable.descendant_pos_x(self)
flowable.add_margin_controller(
MarginController(
staff_flowable_x, self.unit(StaffGroup.RIGHT_PADDING), "_neoscore_staff"
)
)
for clef_x, clef in self.clefs:
flowable_x = staff_flowable_x + clef_x
margin_needed = clef.bounding_rect.width
if margin_needed != ZERO:
# Some clefs can be zero-width; don't add padding in that case.
margin_needed += self.unit(StaffGroup.CLEF_LEFT_PADDING)
flowable.add_margin_controller(
MarginController(flowable_x, margin_needed, "_neoscore_clef")
)
# Assume that key signatures have the same width in all clefs
for key_sig_x, key_sig in self.key_signatures:
flowable_x = staff_flowable_x + key_sig_x
flowable.add_margin_controller(
MarginController(
flowable_x,
key_sig.visual_width + self.unit(StaffGroup.KEY_SIG_LEFT_PADDING),
"_neoscore_key_signature",
)
)
for time_sig in self.descendants_with_attribute(
"_neoscore_time_signature_type_marker"
):
flowable_x = flowable.descendant_pos_x(time_sig)
flowable.add_margin_controller(
MarginController(
flowable_x,
time_sig.visual_width + self.unit(StaffGroup.TIME_SIG_LEFT_PADDING),
"_neoscore_time_signature",
)
)
# Cancel the margin controller immediately after it, this way time
# signatures only affect margins if they lie right around a line start.
flowable.add_margin_controller(
MarginController(flowable_x + Unit(1), ZERO, "_neoscore_time_signature")
)
[docs] def fringe_layout_for_isolated_staff(
self, location: Optional[NewLine]
) -> StaffFringeLayout:
if location:
staff_pos_x = location.flowable_x - self.flowable.descendant_pos_x(self)
if staff_pos_x < ZERO:
# This happens on the first line of a staff positioned at x>0 relative
# to its flowable.
staff_pos_x = ZERO
else:
staff_pos_x = ZERO
# Work right-to-left through different fringe layers
current_x = -self.unit(StaffGroup.RIGHT_PADDING)
clef = self.active_clef_at(staff_pos_x)
key_sig = self.active_key_signature_at(staff_pos_x)
time_sig = next(
(sig for x, sig in self.time_signatures if x == staff_pos_x), None
)
clef_fringe_pos = current_x
key_signature_fringe_pos = current_x
time_signature_fringe_pos = current_x
if time_sig:
current_x -= time_sig.visual_width
time_signature_fringe_pos = current_x
current_x -= self.unit(StaffGroup.TIME_SIG_LEFT_PADDING)
if key_sig:
current_x -= key_sig.visual_width
key_signature_fringe_pos = current_x
current_x -= self.unit(StaffGroup.KEY_SIG_LEFT_PADDING)
if clef:
clef_width = clef.bounding_rect.width
current_x -= clef_width
clef_fringe_pos = current_x
if clef_width != ZERO:
# Some clefs can be zero-width (invisible) - don't pad in this case.
current_x -= self.unit(StaffGroup.CLEF_LEFT_PADDING)
staff_fringe_pos = current_x
return StaffFringeLayout(
staff_pos_x,
staff_fringe_pos,
clef_fringe_pos,
key_signature_fringe_pos,
time_signature_fringe_pos,
)