from __future__ import annotations
import math
from typing import Optional
from neoscore.core.brush import Brush
from neoscore.core.directions import DirectionX
from neoscore.core.music_font import MusicFont
from neoscore.core.music_path import MusicPath
from neoscore.core.pen import Pen
from neoscore.core.point import Point, PointDef
from neoscore.core.positioned_object import PositionedObject
from neoscore.core.spanner_2d import Spanner2D
from neoscore.core.units import ZERO, Unit
[docs]class Hairpin(Spanner2D, MusicPath):
"""A crescendo/diminuendo hairpin spanner.
While this is a path, it requires a music font from which to
derive its appearance.
"""
[docs] def __init__(
self,
pos: PointDef,
parent: PositionedObject,
end_pos: PointDef,
end_parent: Optional[PositionedObject] = None,
direction: DirectionX = DirectionX.RIGHT,
width: Optional[Unit] = None,
font: Optional[MusicFont] = None,
):
"""
Args:
pos: The starting point.
parent: The parent for the starting position. If no font is given,
this or one of its ancestors must implement :obj:`.HasMusicFont`.
end_pos: The stopping point.
end_parent: The parent for the ending position.
If ``None``, defaults to ``self``.
direction: The direction of the hairpin, where ``LEFT`` means diminuendo (>)
and ``RIGHT`` means crescendo (<).
width: The width of the wide hairpin. Defaults to 1 staff unit.
font: If provided, this overrides any font found in the ancestor chain.
"""
MusicPath.__init__(self, pos, parent, font=font, brush=Brush.no_brush())
end_pos = Point.from_def(end_pos)
Spanner2D.__init__(self, end_pos, end_parent or self)
self.direction = direction
self.width = width if width is not None else self.music_font.unit(1)
self.pen = Pen(thickness=self.music_font.engraving_defaults["hairpinThickness"])
self._draw_path()
@property
def music_font(self) -> MusicFont:
return self._music_font
@property
def direction(self) -> DirectionX:
"""The direction of the hairpin.
``LEFT`` means diminuendo (>) and ``RIGHT`` means crescendo (<).
"""
return self._direction
@direction.setter
def direction(self, value: DirectionX):
self._direction = value
def _find_hairpin_points(
self,
) -> Tuple[
Point, PositionedObject, Point, PositionedObject, Point, PositionedObject
]:
"""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 == DirectionX.LEFT:
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 = joint_parent.map_to(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 _draw_path(self):
(
first_pos,
first_parent,
mid_pos,
mid_parent,
last_pos,
last_parent,
) = self._find_hairpin_points()
self.move_to(first_pos.x, first_pos.y, first_parent)
self.line_to(mid_pos.x, mid_pos.y, mid_parent)
self.line_to(last_pos.x, last_pos.y, last_parent)