def brush(self, value: SimpleBrushDef): if value: self._brush = brush_from_simple_def(value) if isinstance(value, str): self._brush = Brush(value) elif isinstance(value, Brush): self._brush = value else: raise TypeError else: self._brush = Brush.from_existing(DEFAULT_BRUSH)
def __init__( self, start: PointDef, start_parent: GraphicObject, stop: PointDef, stop_parent: Optional[GraphicObject], direction: int = -1, ): """ Args: start: The starting point. start_parent: The parent for the starting position. Must be a staff or in one. stop: The stopping point. stop_parent: The parent for the ending position. If `None`, defaults to `self`. direction: The direction of the slur, where `-1` indicates curving upward, and `1` vice versa. """ Path.__init__(self, start, parent=start_parent, brush=Brush((0, 0, 0, 255))) StaffObject.__init__(self, self.parent) stop = Point.from_def(stop) Spanner2D.__init__(self, stop, stop_parent or self) self.direction = direction # Load relevant engraving defaults from music font engraving_defaults = self.staff.music_font.engraving_defaults self.midpoint_thickness = self.staff.unit( engraving_defaults["slurMidpointThickness"]) self.endpoint_thickness = self.staff.unit( engraving_defaults["slurEndpointThickness"]) self._draw_path()
def test_init(self): mock_parent = InvisibleObject(ORIGIN, parent=None) test_pen = Pen("#eeeeee") test_brush = Brush("#dddddd") path = Path((Unit(5), Unit(6)), test_pen, test_brush, mock_parent) assert path.pos == Point(Unit(5), Unit(6)) assert path.pen == test_pen assert path.brush == test_brush
def __init__( self, pos: PointDef, pen: Optional[SimplePenDef] = None, brush: Optional[SimpleBrushDef] = None, parent: Optional[Parent] = None, ): """ Args: pos: The position of the path root. pen: The pen to draw outlines with. brush: The brush to draw outlines with. parent: The parent object or None """ brush = brush or Brush.from_existing(Path._default_brush) super().__init__(pos, ZERO, pen, brush, parent) self.elements: list[PathElement] = []
def test_setters_update_interface(self): brush = Brush(Color("#000000"), BrushPattern.DENSE_1) brush.color = Color("#ffffff") assert brush.interface.color == Color("#ffffff") brush.pattern = BrushPattern.SOLID assert brush.interface.pattern == BrushPattern.SOLID
def test_interface_generation(self): brush = Brush(Color("#ffffff"), BrushPattern.DENSE_1) assert brush.interface == BrushInterface(Color("#ffffff"), BrushPattern.DENSE_1)
def test_from_existing(self): original = Brush(Color("#ffffff"), BrushPattern.DENSE_1) clone = Brush.from_existing(original) assert id(original) != id(clone) assert original.color == clone.color assert original.pattern == clone.pattern
def test_init_with_pattern(self): brush = Brush("#ffffff", BrushPattern.DENSE_1) assert brush.pattern == BrushPattern.DENSE_1
def test_pattern_defaults_to_solid_color(self): brush = Brush("#ffffff") assert brush.pattern == BrushPattern.SOLID
def test_init_with_hex_color(self): brush = Brush("#eeddcc") assert brush.color == Color(238, 221, 204, 255)
class Path(GraphicObject): """A vector path whose points can be anchored to other objects. If a Path is in a `Flowable`, any point anchors in the path should be anchored to objects in the same `Flowable`, or undefined behavior may occur. Likewise, if a Path is not in a `Flowable`, all point anchors should not be in one either. """ _default_brush = Brush( constants.DEFAULT_PATH_BRUSH_COLOR, constants.DEFAULT_BRUSH_PATTERN ) def __init__( self, pos: PointDef, pen: Optional[SimplePenDef] = None, brush: Optional[SimpleBrushDef] = None, parent: Optional[Parent] = None, ): """ Args: pos: The position of the path root. pen: The pen to draw outlines with. brush: The brush to draw outlines with. parent: The parent object or None """ brush = brush or Brush.from_existing(Path._default_brush) super().__init__(pos, ZERO, pen, brush, parent) self.elements: list[PathElement] = [] ######## CLASSMETHODS ######## @classmethod def straight_line( cls, start: PointDef, stop: PointDef, pen: Optional[SimplePenDef] = None, brush: Optional[SimpleBrushDef] = None, parent: Parent = None, ) -> Path: """Path: Constructor for a straight line Args: start: Starting position relative to the parent stop: Ending position relative to the parent. pen: The pen to draw outlines with. brush: The brush to draw outlines with. parent: The parent object or None """ line = cls(start, pen, brush, parent) if isinstance(stop, tuple): stop = Point(*stop) line.line_to(stop.x, stop.y) return line ######## PUBLIC PROPERTIES ######## @property def length(self) -> Unit: """The breakable length of the path. This is calculated automatically from path contents. By extension, this means that by default all `Path` objects will automatically wrap in `Flowable`s. """ # Find the positions of every path element relative to the path min_x = Unit(float("inf")) max_x = Unit(-float("inf")) for element in self.elements: # Determine element X relative to self relative_x = map_between_x(self, element) # Now update min/max accordingly if relative_x > max_x: max_x = relative_x if relative_x < min_x: min_x = relative_x return max_x - min_x ######## Public Methods ######## def line_to(self, x: Unit, y: Unit, parent: Optional[Parent] = None): """Draw a path from the current position to a new point. A point parent may be passed as well, anchored the target point to a separate GraphicObject. In this case, the coordinates passed will be considered relative to the parent. If the path is empty, this will add two elements, an initial `MoveTo(Point(Unit(0), Unit(0)), self)` and the requested `LineTo`. Args: x: The end x position y: The end y position parent: An optional parent, whose position the target coordinate will be relative to. """ if not len(self.elements): # Needed to ensure bounding rect / length calculations are correct self.elements.append(MoveTo(Point(Unit(0), Unit(0)), self)) self.elements.append(LineTo(Point(x, y), parent or self)) def move_to(self, x: Unit, y: Unit, parent: Optional[Parent] = None): """Close the current sub-path and start a new one. A point parent may be passed as well, anchored the target point to a separate `GraphicObject`. In this case, the coordinates passed will be considered relative to the parent. Args: x: The end x position y: The end y position parent: An optional parent, whose position the target coordinate will be relative to. """ self.elements.append(MoveTo(Point(x, y), parent or self)) def close_subpath(self): """Close the current sub-path and start a new one at the local origin. This is equivalent to `move_to(Unit(0), Unit(0))` Note: This convenience method does not support point parentage. If you need to anchor the new point, use an explicit `move_to(Unit(0), Unit(0), parent)` instead. """ self.move_to(ZERO, ZERO) def cubic_to( self, control_1_x: Unit, control_1_y: Unit, control_2_x: Unit, control_2_y: Unit, end_x: Unit, end_y: Unit, control_1_parent: Optional[Parent] = None, control_2_parent: Optional[Parent] = None, end_parent: Optional[Parent] = None, ): """Draw a cubic bezier curve from the current position to a new point. If the path is empty, this will add two elements, an initial `MoveTo(Point(Unit(0), Unit(0)), self)` and the requested `CurveTo`. Args: control_1_x: The x coordinate of the first control point. control_1_y: The y coordinate of the first control point. control_2_x: The x coordinate of the second control point. control_2_y: The y coordinate of the second control point. end_x: The x coordinate of the curve target. end_y: The y coordinate of the curve target. control_1_parent: An optional parent for the first control point. Defaults to `self`. control_2_parent: An optional parent for the second control point. Defaults to `self`. end_parent: An optional parent for the curve target. Defaults to `self`. """ c1 = ControlPoint( Point(control_1_x, control_1_y), control_1_parent or self, ) c2 = ControlPoint( Point(control_2_x, control_2_y), control_2_parent or self, ) if not len(self.elements): # Needed to ensure bounding rect / length calculations are correct self.elements.append(MoveTo(Point(Unit(0), Unit(0)), self)) self.elements.append(CurveTo(c1, c2, Point(end_x, end_y), end_parent or self)) def _relative_element_pos(self, element: Parent) -> Point: return map_between(self, element) def _resolve_path_elements(self) -> list[ResolvedPathElement]: resolved: list[ResolvedPathElement] = [] for element in self.elements: # Interface drawing methods expect coordinates # relative to PathInterface root pos = self._relative_element_pos(element) if isinstance(element, LineTo): resolved.append(ResolvedLineTo(pos.x, pos.y)) elif isinstance(element, MoveTo): resolved.append(ResolvedMoveTo(pos.x, pos.y)) elif isinstance(element, CurveTo): element = cast(CurveTo, element) resolved_c1_pos = self._relative_element_pos(element.control_1) resolved_c2_pos = self._relative_element_pos(element.control_2) resolved.append( ResolvedCurveTo( resolved_c1_pos.x, resolved_c1_pos.y, resolved_c2_pos.x, resolved_c2_pos.y, pos.x, pos.y, ) ) else: raise TypeError("Unknown PathElement type") return resolved def _render_slice( self, pos: Point, clip_start_x: Optional[Unit] = None, clip_width: Optional[Unit] = None, ): # If this proves to be a performance bottleneck in the future, # it is very possible to optimize this to create `PathInterface`s # which reuse `QPainterPath`s. resolved_path_elements = self._resolve_path_elements() slice_interface = PathInterface( pos, self.pen.interface, self.brush.interface, resolved_path_elements, clip_start_x, clip_width, ) slice_interface.render() self.interfaces.append(slice_interface) def _render_complete( self, pos: Point, dist_to_line_start: Optional[Unit] = None, local_start_x: Optional[Unit] = None, ): self._render_slice(pos, None, None) def _render_before_break( self, local_start_x: Unit, start: Point, stop: Point, dist_to_line_start: Unit ): self._render_slice(start, ZERO, stop.x - start.x) def _render_after_break(self, local_start_x: Unit, start: Point, stop: Point): self._render_slice(start, local_start_x, stop.x - start.x) def _render_spanning_continuation( self, local_start_x: Unit, start: Point, stop: Point ): self._render_slice(start, local_start_x, stop.x - start.x)