def __init__( self, pos_x: Unit, pitch: PitchDef, duration: BeatDef, parent: GraphicObject ): """ Args: pos_x (Unit): The x-axis position relative to `parent`. The y-axis position is calculated automatically based on `pitch` and contextual information in `self.staff`. pitch (Pitch or str): May be a `str` pitch representation. See `Pitch` for valid signatures. duration (Beat or init tuple): The logical duration of the notehead. This is used to determine the glyph style. parent (GraphicObject): Must either be a `Staff` or an object with an ancestor `Staff`. """ self._pitch = Pitch.from_def(pitch) self._duration = Beat.from_def(duration) # Use a temporary y-axis position before calculating it for real MusicText.__init__( self, (pos_x, ZERO), [self._glyphnames[self.duration.base_division]], parent, ) StaffObject.__init__(self, parent) self.y = self.staff.unit( self.staff_pos - map_between(self.staff, self.parent).y )
def test_map_between_where_src_parent_is_dest(self): destination = InvisibleObject((Unit(3), Unit(10)), neoscore.document.pages[0]) source = InvisibleObject((Unit(1), Unit(2)), destination) relative_pos = map_between(source, destination) expected = Point(Unit(-1), Unit(-2)) assert_almost_equal(relative_pos, expected)
def _render_occurrence(self, pos: Point, local_start_x: Unit, shift_for_clef: bool): """Render one appearance of one key signature accidental. Much of the positioning code needs to be performed per-occurrence because key signatures can have different appearances when clefs change. Ideally there should be a way to cache/centralize much of this work. """ staff_pos_in_flowable = map_between(self.flowable, self.staff) pos_x_in_staff = local_start_x - staff_pos_in_flowable.x clef = self.staff.active_clef_at(pos_x_in_staff) if clef is None: return clef_type = clef.clef_type pos_tuple = _KeySignatureAccidental.positions[ self.accidental_type][clef_type][self.pitch_letter] visual_pos_x = self.staff.unit(pos_tuple[0]) + pos.x visual_pos_y = self.staff.unit(pos_tuple[1]) + pos.y if shift_for_clef: visual_pos_x += self._padded_clef_width(clef) self._render_slice(Point(visual_pos_x, visual_pos_y))
def test_map_between_with_common_parent(self): parent = InvisibleObject((Unit(5), Unit(6)), neoscore.document.pages[0]) source = InvisibleObject((Unit(1), Unit(2)), parent) destination = InvisibleObject((Unit(3), Unit(10)), parent) relative_pos = map_between(source, destination) expected = Point(Unit(2), Unit(8)) assert_almost_equal(relative_pos, expected)
def vertical_span(self) -> Unit: """StaffUnit: The vertical distance covered by the staves The distance from the top of `self.highest_staff` to the bottom of `self.lowest_staff`, in `self.highest_staff.unit` StaffUnits. """ return self.highest_staff.unit( mapping.map_between(self.highest_staff, self.lowest_staff).y + self.lowest_staff.height)
def test_map_between(self): source = InvisibleObject((Unit(5), Unit(6)), neoscore.document.pages[1]) destination = InvisibleObject((Unit(99), Unit(90)), neoscore.document.pages[4]) relative_pos = map_between(source, destination) page_1_pos = canvas_pos_of(neoscore.document.pages[1]) page_4_pos = canvas_pos_of(neoscore.document.pages[4]) expected = (page_4_pos + Point(Unit(99), Unit(90))) - ( page_1_pos + Point(Unit(5), Unit(6))) assert_almost_equal(relative_pos, expected)
def spanner_2d_length(self) -> Unit: """The 2d length of the spanner. Note: This takes into account both the x and y axis. For only the horizontal length, use `spanner_x_length`. """ if self.end_parent == self: relative_stop = self.end_pos else: relative_stop = ( map_between(cast(Positioned, self), self.end_parent) + self.end_pos) distance = Unit( math.sqrt((relative_stop.x.base_value**2) + (relative_stop.y.base_value**2))) return type(cast(Positioned, self).pos.x)(distance)
def __init__(self, pos_x: Unit, staves: Iterable[Staff]): """ Args: pos_x: The barline position relative to the top staff. staves: """ MultiStaffObject.__init__(self, set(staves)) Path.__init__(self, Point(pos_x, ZERO), parent=self.highest_staff) engraving_defaults = self.highest_staff.music_font.engraving_defaults thickness = engraving_defaults["thinBarlineThickness"] self.pen = Pen(thickness=thickness) # Draw path offset = map_between(self.lowest_staff, self.highest_staff) bottom_x = pos_x + offset.x self.line_to(bottom_x, self.lowest_staff.height, parent=self.lowest_staff)
def end_y(self) -> Unit: """The y position of the endpoint""" return map_between(self.end_parent, cast(Positioned, self)).y
def _relative_element_pos(self, element: Parent) -> Point: return map_between(self, element)
def __init__( self, start: PointDef, start_parent: GraphicObject, end_x: Unit, end_parent: Optional[GraphicObject] = None, indication: str = "8va", ): """ Args: start (Point or tuple init args): start_parent (GraphicObject): An object either in a Staff or a staff itself. This object will become the line's parent. end_x (Unit): The spanner end x position. The y position will be automatically calculated to be horizontal. end_parent (GraphicObject): An object either in a Staff or a staff itself. The root staff of this *must* be the same as the root staff of `start_parent`. If omitted, the stop point is relative to the start point. indication (str): A valid octave indication. currently supported indications are: - '15ma' (two octaves higher) - '8va' (one octave higher) - '8vb' (one octave lower) - '15mb' (two octaves lower) The default value is '8va'. """ ObjectGroup.__init__(self, start, start_parent) Spanner.__init__(self, end_x, end_parent or self) StaffObject.__init__(self, self.parent) self.transposition = Transposition(OctaveLine.intervals[indication]) self.line_text = _OctaveLineText( # No offset relative to ObjectGroup pos=ORIGIN, parent=self, length=self.length, indication=indication, ) # Vertically center the path relative to the text text_rect = self.line_text.bounding_rect # TODO LOW line needs some padding path_x = text_rect.width path_y = text_rect.height / -2 self.line_path = Path( pos=Point(path_x, path_y), pen=Pen( thickness=self.staff.music_font.engraving_defaults[ "octaveLineThickness" ], pattern=PenPattern.DASH, ), parent=self, ) # Drawn main line part self.line_path.line_to(self.end_pos.x, path_y, self.end_parent) pos_relative_to_staff = map_between(self.staff, self) # Draw end hook pointing toward the staff hook_direction = 1 if pos_relative_to_staff.y <= ZERO else -1 self.line_path.line_to( self.end_pos.x, (path_y + self.staff.unit(0.75 * hook_direction)), self.end_parent, )
def test_map_between_with_same_source_and_dest(self): obj = InvisibleObject((Unit(5), Unit(6)), neoscore.document.pages[0]) assert_almost_equal(map_between(obj, obj), Point(Unit(0), Unit(0)))
def _find_hairpin_points( self, ) -> tuple[Point, GraphicObject, Point, GraphicObject, Point, GraphicObject]: """Find the hairpin path points for a set of parameters. The returned tuple is 3 pairs of Points and parents, where the outer 2 represent the wide ends of the hairpin and the middle represents the small end joint. """ if self.direction == -1: joint_pos = self.end_pos joint_parent = self.end_parent end_center_pos = self.pos end_center_parent = self.parent else: joint_pos = self.pos joint_parent = self.parent end_center_pos = self.end_pos end_center_parent = self.end_parent dist = self.width / 2 # Find relative distance from joint to end_center_pos parent_distance = map_between(joint_parent, end_center_parent) relative_stop = parent_distance + end_center_pos - joint_pos if relative_stop.y == ZERO: return ( Point(end_center_pos.x, end_center_pos.y + dist), end_center_parent, joint_pos, joint_parent, Point(end_center_pos.x, end_center_pos.y - dist), end_center_parent, ) elif relative_stop.x == ZERO: return ( Point(end_center_pos.x + dist, end_center_pos.y), end_center_parent, joint_pos, joint_parent, Point(end_center_pos.x - dist, end_center_pos.y), end_center_parent, ) # else ... # Find the two points (self.width / 2) away from the end_center_pos # which lie on the line perpendicular to the spanner line. # Note that there is no risk of division by zero because # previous if / elif statements catch those possibilities center_slope = relative_stop.y / relative_stop.x opening_slope = (center_slope * -1)**-1 opening_y_intercept = (end_center_pos.x * opening_slope) - end_center_pos.y # Find needed x coordinates of outer points # x = dist / sqrt(1 + slope^2) first_x = end_center_pos.x + (dist / math.sqrt(1 + (opening_slope**2))) last_x = end_center_pos.x - (dist / math.sqrt(1 + (opening_slope**2))) # Calculate matching y coordinates from opening line function first_y = (first_x * opening_slope) - opening_y_intercept last_y = (last_x * opening_slope) - opening_y_intercept return ( Point(first_x, first_y), end_center_parent, joint_pos, joint_parent, Point(last_x, last_y), end_center_parent, )
def pos_in_staff(self) -> Point: """The logical position of this object relative to the staff.""" return mapping.map_between(self.staff, cast(mapping.Positioned, self))