Source code for neoscore.western.tab_staff

from __future__ import annotations

from typing import TYPE_CHECKING, Optional, cast

from neoscore.core.layout_controllers import MarginController, NewLine
from neoscore.core.music_font import MusicFont
from neoscore.core.pen import Pen
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.tab_clef import TabClef

_LINE_SPACE_TO_FONT_UNIT_RATIO: float = 2 / 3


[docs]class TabStaff(AbstractStaff): """A staff for writing guitar tablature with any number of strings. This class is not suitable for use with :obj:`.Chordrest`, :obj:`.Clef`, :obj:`.TimeSignature`, and other such classes dependent on classical staff semantics. While ``TabStaff`` has a :obj:`.MusicFont`, its unit does not necessarily correspond to the distance between staff lines. By default, the line spacing is wider than classical staves, and its ``MusicFont`` is sized to 2/3 that spacing. """
[docs] def __init__( self, pos: PointDef, parent: Optional[PositionedObject], length: Unit, group: Optional[StaffGroup] = None, line_spacing: Unit = Mm(2.5), line_count: int = 6, music_font: Optional[MusicFont] = None, pen: Optional[Pen] = 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: The font to use for :obj:`.MusicText` objects in the staff. Unlike in :obj:`.Staff`, this font's ``unit`` is not necessarily equivalent to the space between two staff (string) lines. By default, this will use the system-wide default music font with a unit sized to 2/3 the staff line spacing. pen: The pen used to draw the staff lines. Defaults to a line with thickness from the music font's engraving default. """ music_font = music_font or MusicFont( "Bravura", make_unit_class( "TabStaffTextUnit", line_spacing.base_value * _LINE_SPACE_TO_FONT_UNIT_RATIO, ), ) pen = pen or Pen(thickness=music_font.engraving_defaults["staffLineThickness"]) super().__init__( pos, parent, length, group, line_spacing, line_count, music_font, pen )
[docs] def string_y(self, string: int) -> Unit: """Return the Y position of a given string's line. Strings are indicated from 1 to N, where 1 is the top string and N is the bottom. """ return self.line_spacing * (string - 1)
@property def font_to_staff_space_ratio(self) -> float: """Conversion ratio between the font's unit and the staff line spacing.""" return cast(float, self.unit(1) / self.line_spacing) @render_cached_property def clefs(self) -> List[Tuple[Unit, TabClef]]: """All the clefs in this staff, ordered by their relative x pos.""" return self.find_ordered_descendants_with_attr("_neoscore_tab_clef_type_marker")
[docs] def active_clef_at(self, pos_x: Unit) -> Optional[TabClef]: """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, )
[docs] def register_layout_controllers(self): """Register any flowable margin controllers needed by the staff. Staff subclasses must implement this. """ 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 + self.unit( StaffGroup.CLEF_LEFT_PADDING ) flowable.add_margin_controller( MarginController(flowable_x, margin_needed, "neoscore_clef") )
[docs] def fringe_layout_for_isolated_staff( self, location: Optional[NewLine] ) -> StaffFringeLayout: """Determine the staff fringe layout of this staff in isolation. This is the layout needed if this staff was alone in a staff system. ``StaffGroup`` uses this as a starting point in its fringe layout logic. Staff subclasses must implement this. """ 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_fringe_pos = current_x clef = self.active_clef_at(staff_pos_x) if clef: current_x -= clef.bounding_rect.width clef_fringe_pos = current_x current_x -= self.unit(StaffGroup.CLEF_LEFT_PADDING) staff_fringe_pos = current_x return StaffFringeLayout( staff_pos_x, staff_fringe_pos, clef_fringe_pos, ZERO, ZERO )