API documentation
Every class LineDream exports, generated from the docstrings of the current release (v0.4.0).
LineDream
View Source
from .primitives.Line import Line
from .primitives.BaseShape import BaseShape
from .primitives.Ellipse import Circle, Ellipse, Point, Arc
from .primitives.Rectangle import Rectangle, Square
from .primitives.Group import Group
from .primitives.Text import Text
from .environment.Canvas import _Canvas
from .helpers.CircleMath import CircleMath
# from .primitives.TextLine import TextLine
# from .environment.Tweaks import Tweaks
Canvas = _Canvas
"""
Canvas Singleton Object. This controls the output 'canvas', and rendering of the draw loop.
### Attributes
- width
- height
"""
# Note, I had to do this Canvas=_Canvas thing so these ^ doc strings would work.
__all__ = [
'Line',
'Circle',
'Ellipse',
'Arc',
'Point',
'Rectangle',
'Square',
'Group',
'CircleMath',
'BaseShape',
'Canvas',
'Text'
]
View Source
class Line(BaseShape):
'''This is how you create a line with two or more vertices'''
def __init__(self, vertices:List[Tuple]=None, **kwargs):
super().__init__(**kwargs)
if vertices:
self._set_vertices(vertices)
@classmethod
def from_array(cls, xy, **kwargs):
'''Create a line from an (N, 2) or (N, 3) array-like of x, y[, z] coordinates.
Builds the vertex array in one step, so it is fast for long polylines.'''
line = cls(**kwargs)
line._set_vertices(xy)
return line
def _set_vertices(self, xy):
xy = np.asarray(xy, dtype=float)
if xy.ndim != 2 or xy.shape[1] not in (2, 3):
raise ValueError(f'Expected an (N, 2) or (N, 3) array of vertices, got shape {xy.shape}')
z = xy[:, 2] if xy.shape[1] == 3 else np.zeros(len(xy))
self._vertices = np.column_stack([xy[:, 0], xy[:, 1], z, np.ones(len(xy))])
This is how you create a line with two or more vertices
View Source
def __init__(self, vertices:List[Tuple]=None, **kwargs):
super().__init__(**kwargs)
if vertices:
self._set_vertices(vertices)
View Source
@classmethod
def from_array(cls, xy, **kwargs):
'''Create a line from an (N, 2) or (N, 3) array-like of x, y[, z] coordinates.
Builds the vertex array in one step, so it is fast for long polylines.'''
line = cls(**kwargs)
line._set_vertices(xy)
return line
Create a line from an (N, 2) or (N, 3) array-like of x, y[, z] coordinates. Builds the vertex array in one step, so it is fast for long polylines.
View Source
class Circle(Ellipse):
def __init__(self,x,y, radius, **kwargs):
super().__init__(x,y, radius, radius, **kwargs)
def scale(self, amt, _amt_y=None):
"""Scale the circle by the given amount.
:param amt: Scale factor (2.0 = twice as big)
"""
self.radius_x = self.radius_x * amt
self.radius_y = self.radius_y * amt
return self
This is how to create an ellipse
View Source
def __init__(self,x,y, radius, **kwargs):
super().__init__(x,y, radius, radius, **kwargs)
This is the init for creating an __init__ :param x: x coord of center :param y: y coord of center :param radius_x: radius x :param radius_y: radius y :param kwargs: style kwargs
.. todo:: THIS NEEDS WORK
View Source
def scale(self, amt, _amt_y=None):
"""Scale the circle by the given amount.
:param amt: Scale factor (2.0 = twice as big)
"""
self.radius_x = self.radius_x * amt
self.radius_y = self.radius_y * amt
return self
Scale the circle by the given amount.
:param amt: Scale factor (2.0 = twice as big)
View Source
class Ellipse(BaseShape):
'''This is how to create an ellipse'''
def __init__(self,x,y, radius_x, radius_y, **kwargs):
'''This is the init for creating an __init__
:param x: x coord of center
:param y: y coord of center
:param radius_x: radius x
:param radius_y: radius y
:param kwargs: style kwargs
.. todo:: THIS NEEDS WORK
'''
super().__init__(**kwargs)
self.x = x
""" x coord of center"""
self.y = y
self.radius_x = radius_x
self.radius_y = radius_y
self.is_circle=True
def add_vertex(self, *coords):
raise Exception("Ellipses do not have vertexes")
def transform(self, x=0, y=0, z=0):
'''
Translate the ellipse by x/y/z
:param x:
:param y:
:param z:
:return:
'''
self.x = self.x + x
self.y = self.y + y
return self
def scale(self, amt_x, amt_y=None, **kwargs):
"""Scale the ellipse by the given amount.
:param amt_x: Scale factor for x-axis (2.0 = twice as big)
:param amt_y: Scale factor for y-axis (defaults to amt_x for uniform scaling)
"""
if amt_y is None:
amt_y = amt_x
self.radius_x = self.radius_x * amt_x
self.radius_y = self.radius_y * amt_y
return self
@property
def svg_object(self):
return drawsvg.Ellipse(self.x, self.y, self.radius_x, self.radius_y,
fill=self.fill_color, stroke=self.stroke_color,
stroke_width=self.stroke_width, fill_opacity=self.fill_opacity)
This is how to create an ellipse
View Source
def __init__(self,x,y, radius_x, radius_y, **kwargs):
'''This is the init for creating an __init__
:param x: x coord of center
:param y: y coord of center
:param radius_x: radius x
:param radius_y: radius y
:param kwargs: style kwargs
.. todo:: THIS NEEDS WORK
'''
super().__init__(**kwargs)
self.x = x
""" x coord of center"""
self.y = y
self.radius_x = radius_x
self.radius_y = radius_y
self.is_circle=True
This is the init for creating an __init__ :param x: x coord of center :param y: y coord of center :param radius_x: radius x :param radius_y: radius y :param kwargs: style kwargs
.. todo:: THIS NEEDS WORK
x coord of center
View Source
def add_vertex(self, *coords):
raise Exception("Ellipses do not have vertexes")
Append a set of vertexes to the primitive shape
View Source
def transform(self, x=0, y=0, z=0):
'''
Translate the ellipse by x/y/z
:param x:
:param y:
:param z:
:return:
'''
self.x = self.x + x
self.y = self.y + y
return self
Translate the ellipse by x/y/z :param x: :param y: :param z: :return:
View Source
def scale(self, amt_x, amt_y=None, **kwargs):
"""Scale the ellipse by the given amount.
:param amt_x: Scale factor for x-axis (2.0 = twice as big)
:param amt_y: Scale factor for y-axis (defaults to amt_x for uniform scaling)
"""
if amt_y is None:
amt_y = amt_x
self.radius_x = self.radius_x * amt_x
self.radius_y = self.radius_y * amt_y
return self
Scale the ellipse by the given amount.
:param amt_x: Scale factor for x-axis (2.0 = twice as big) :param amt_y: Scale factor for y-axis (defaults to amt_x for uniform scaling)
View Source
class Arc(BaseShape):
'''This is how to create an ellipse'''
def __init__(self, x, y, radius, start_angle, end_angle, x_y_start_coords=False, **kwargs):
'''This is the init for creating an __init__
:param x: x coord of center
:param y: y coord of center
:param radius_x: radius x
:param radius_y: radius y
:param x_y_start_coords: If true, this will use the x,y params as where the 0 degree location is - not the center of the arc. For example:
If you specify Arc(10, 15, 3, 0, 270) the arc start will be at (10,15).
:param kwargs: style kwargs
.. todo:: THIS NEEDS WORK
'''
super().__init__(**kwargs)
if x_y_start_coords:
d_x, d_y = CircleMath.distance_to_coords(start_angle, radius)
x = x-d_x
y = y-d_y
self.x = x
self.y = y
self.radius = radius
self.start_angle = start_angle
self.end_angle = end_angle
self.is_arc = True
@property
def start_coords(self):
'''For the give arc, get the x,y coordinates of the start of the arc'''
d_x, d_y = CircleMath.distance_to_coords(self.start_angle, self.radius)
return self.x + d_x, self.y +d_y
@property
def end_coords(self):
'''For the give arc, get the x,y coordinates of the end start of the arc'''
d_x, d_y = CircleMath.distance_to_coords(self.end_angle, self.radius)
return self.x + d_x, self.y + d_y
def scale(self, amt, _amt_y=None, origin=None):
"""Scale the arc by the given amount.
:param amt: Scale factor (2.0 = twice as big)
"""
self.radius = self.radius * amt
return self
def rotate(self, theta, origin=None, axis=None):
self.start_angle +=theta
self.end_angle += theta
return self
@property
def svg_object(self):
return drawsvg.Arc(cx=self.x, cy=self.y, r=self.radius, cw=True,
start_deg=self.start_angle, end_deg=self.end_angle,
fill=self.fill_color, stroke=self.stroke_color,
stroke_width=self.stroke_width, fill_opacity=self.fill_opacity,
# close=shape.close_path
)
This is how to create an ellipse
View Source
def __init__(self, x, y, radius, start_angle, end_angle, x_y_start_coords=False, **kwargs):
'''This is the init for creating an __init__
:param x: x coord of center
:param y: y coord of center
:param radius_x: radius x
:param radius_y: radius y
:param x_y_start_coords: If true, this will use the x,y params as where the 0 degree location is - not the center of the arc. For example:
If you specify Arc(10, 15, 3, 0, 270) the arc start will be at (10,15).
:param kwargs: style kwargs
.. todo:: THIS NEEDS WORK
'''
super().__init__(**kwargs)
if x_y_start_coords:
d_x, d_y = CircleMath.distance_to_coords(start_angle, radius)
x = x-d_x
y = y-d_y
self.x = x
self.y = y
self.radius = radius
self.start_angle = start_angle
self.end_angle = end_angle
self.is_arc = True
This is the init for creating an __init__ :param x: x coord of center :param y: y coord of center :param radius_x: radius x :param radius_y: radius y :param x_y_start_coords: If true, this will use the x,y params as where the 0 degree location is - not the center of the arc. For example: If you specify Arc(10, 15, 3, 0, 270) the arc start will be at (10,15). :param kwargs: style kwargs
.. todo:: THIS NEEDS WORK
For the give arc, get the x,y coordinates of the start of the arc
For the give arc, get the x,y coordinates of the end start of the arc
View Source
def scale(self, amt, _amt_y=None, origin=None):
"""Scale the arc by the given amount.
:param amt: Scale factor (2.0 = twice as big)
"""
self.radius = self.radius * amt
return self
Scale the arc by the given amount.
:param amt: Scale factor (2.0 = twice as big)
View Source
def rotate(self, theta, origin=None, axis=None):
self.start_angle +=theta
self.end_angle += theta
return self
Rotate the shape by the given angle along the given axis.
:param theta: The angle by which to rotate (in degrees) :type theta: float
:param axis: The axis along which to rotate (defaults to the z-axis) :type axis: np.ndarray or list
View Source
class Point(Circle):
def __init__(self,x,y, **kwargs):
super().__init__(x,y, .5, **kwargs)
def scale(self, amt=1, _amt_y=None):
"""Points cannot be scaled. This is a no-op that returns self for method chaining."""
import sys
sys.stderr.write("Warning: Points cannot be scaled. Operation ignored.\n")
return self
This is how to create an ellipse
View Source
def __init__(self,x,y, **kwargs):
super().__init__(x,y, .5, **kwargs)
This is the init for creating an __init__ :param x: x coord of center :param y: y coord of center :param radius_x: radius x :param radius_y: radius y :param kwargs: style kwargs
.. todo:: THIS NEEDS WORK
View Source
def scale(self, amt=1, _amt_y=None):
"""Points cannot be scaled. This is a no-op that returns self for method chaining."""
import sys
sys.stderr.write("Warning: Points cannot be scaled. Operation ignored.\n")
return self
Points cannot be scaled. This is a no-op that returns self for method chaining.
View Source
class Rectangle(BaseShape):
def __init__(self, c_x, c_y, width, height, **kwargs):
"""
Creates a Rectangle Primitive
Parameters
----------
c_x : int
center of rectangle x coord
c_y: int
center of rectangle y coord
:param width:
:param height:
:param kwargs:
"""
super().__init__(**kwargs)
self.c_x = c_x
self.c_y = c_y
self.width = width
self.height = height
self.close_path = True
# top left
t_l_x = c_x - (width/2)
t_l_y = c_y - (height/2)
# top right
t_r_x = c_x + (width/2)
t_r_y = c_y - (height/2)
# bottom right
b_r_x = c_x + (width / 2)
b_r_y = c_y + (height / 2)
# bottom left
b_l_x = c_x - (width/2)
b_l_y = c_y + (height/2)
# this order is very important.
# It adds the vertex's in a clockwise rotation
self.add_vertex(t_l_x, t_l_y)
self.add_vertex(t_r_x, t_r_y)
self.add_vertex(b_r_x, b_r_y)
self.add_vertex(b_l_x, b_l_y)
All provided primitives inherit from this class. If a user wants to make their own primitive, its recommended to inherit from this class so the new primative is added to the draw queue.
View Source
def __init__(self, c_x, c_y, width, height, **kwargs):
"""
Creates a Rectangle Primitive
Parameters
----------
c_x : int
center of rectangle x coord
c_y: int
center of rectangle y coord
:param width:
:param height:
:param kwargs:
"""
super().__init__(**kwargs)
self.c_x = c_x
self.c_y = c_y
self.width = width
self.height = height
self.close_path = True
# top left
t_l_x = c_x - (width/2)
t_l_y = c_y - (height/2)
# top right
t_r_x = c_x + (width/2)
t_r_y = c_y - (height/2)
# bottom right
b_r_x = c_x + (width / 2)
b_r_y = c_y + (height / 2)
# bottom left
b_l_x = c_x - (width/2)
b_l_y = c_y + (height/2)
# this order is very important.
# It adds the vertex's in a clockwise rotation
self.add_vertex(t_l_x, t_l_y)
self.add_vertex(t_r_x, t_r_y)
self.add_vertex(b_r_x, b_r_y)
self.add_vertex(b_l_x, b_l_y)
Creates a Rectangle Primitive
Parameters
- c_x (int): center of rectangle x coord
- c_y (int): center of rectangle y coord
- :param width:
- :param height:
- :param kwargs:
View Source
class Square(Rectangle):
def __init__(self, c_x: int, c_y: int, width, **kwargs):
"""
Create a square primitive
:param c_x: center of square x coordinate
:param c_y: center of square y coordinate
:param width: height/width of the square.
:param kwargs: style dict
"""
super().__init__(c_x, c_y, width, width, **kwargs)
All provided primitives inherit from this class. If a user wants to make their own primitive, its recommended to inherit from this class so the new primative is added to the draw queue.
View Source
def __init__(self, c_x: int, c_y: int, width, **kwargs):
"""
Create a square primitive
:param c_x: center of square x coordinate
:param c_y: center of square y coordinate
:param width: height/width of the square.
:param kwargs: style dict
"""
super().__init__(c_x, c_y, width, width, **kwargs)
Create a square primitive
:param c_x: center of square x coordinate :param c_y: center of square y coordinate :param width: height/width of the square. :param kwargs: style dict
View Source
class Group(BaseShape):
'''This will create a group. It also adds inkscape/axidraw compatible attributes
:param label: The label of the group
:param id: The id of the group
'''
def __init__(self,label=None,id=None, **kwargs):
super().__init__(**kwargs)
self.is_group = True
if label == None:
label = str(uuid.uuid4())
self.label = label
if id == None:
id = label
self.id = id
self.items = []
@property
def svg_label(self):
return f'inkscape:label="{self.label}"'
def add_item(self, item):
'''Add a shape to the group'''
self.items.append(item)
_Canvas.remove_by_id(item.id)
def extend(self, items):
'''Add many shapes to the group at once. Removes them from the draw queue in a single pass,
so it stays fast for thousands of items (add_item scans the queue once per item).'''
items = list(items)
self.items.extend(items)
ids = {item.id for item in items}
_Canvas.draw_queue = [q for q in _Canvas.draw_queue if q.id not in ids]
This will create a group. It also adds inkscape/axidraw compatible attributes :param label: The label of the group :param id: The id of the group
View Source
def __init__(self,label=None,id=None, **kwargs):
super().__init__(**kwargs)
self.is_group = True
if label == None:
label = str(uuid.uuid4())
self.label = label
if id == None:
id = label
self.id = id
self.items = []
View Source
def add_item(self, item):
'''Add a shape to the group'''
self.items.append(item)
_Canvas.remove_by_id(item.id)
Add a shape to the group
View Source
def extend(self, items):
'''Add many shapes to the group at once. Removes them from the draw queue in a single pass,
so it stays fast for thousands of items (add_item scans the queue once per item).'''
items = list(items)
self.items.extend(items)
ids = {item.id for item in items}
_Canvas.draw_queue = [q for q in _Canvas.draw_queue if q.id not in ids]
Add many shapes to the group at once. Removes them from the draw queue in a single pass, so it stays fast for thousands of items (add_item scans the queue once per item).
View Source
class CircleMath:
'''
A class with helper methods for circles
'''
@staticmethod
def distance_to_coords(degrees, distance):
angle_radians = degrees * math.pi / 180
x = math.cos(angle_radians) * distance
y = math.sin(angle_radians) * distance
return x, y
@staticmethod
def rotate(p, origin=(0, 0), degrees=0):
# https://stackoverflow.com/a/58781388/6775715
angle = np.deg2rad(degrees)
R = np.array([[np.cos(angle), -np.sin(angle)],
[np.sin(angle), np.cos(angle)]])
o = np.atleast_2d(origin)
p = np.atleast_2d(p)
rv = np.squeeze((R @ (p.T - o.T) + o.T).T)
#todo: remove this once native numpy arrays are supported
return rv.tolist()
@staticmethod
def degrees_to_radians(degrees):
return (math.radians(degrees / math.pi))
A class with helper methods for circles
View Source
@staticmethod
def distance_to_coords(degrees, distance):
angle_radians = degrees * math.pi / 180
x = math.cos(angle_radians) * distance
y = math.sin(angle_radians) * distance
return x, y
View Source
@staticmethod
def rotate(p, origin=(0, 0), degrees=0):
# https://stackoverflow.com/a/58781388/6775715
angle = np.deg2rad(degrees)
R = np.array([[np.cos(angle), -np.sin(angle)],
[np.sin(angle), np.cos(angle)]])
o = np.atleast_2d(origin)
p = np.atleast_2d(p)
rv = np.squeeze((R @ (p.T - o.T) + o.T).T)
#todo: remove this once native numpy arrays are supported
return rv.tolist()
View Source
@staticmethod
def degrees_to_radians(degrees):
return (math.radians(degrees / math.pi))
View Source
class BaseShape(object):
'''All provided primitives inherit from this class. If a user wants to make their own primitive, its recommended to
inherit from this class so the new primative is added to the draw queue.'''
def __init__(self,**kwargs):
self._vertices = np.array([])
self._fill_color='none'
self._stroke_color='black'
self._stroke_width=1
self._close_path = False
self._fill_opacity = 1.0
self.is_circle=False
self.is_arc=False
self.is_group = False
self.is_text = False
self.id = str(uuid.uuid4())
for k,v in kwargs.items():
if hasattr(self, k):
setattr(self, k, v)
else:
raise AttributeError(f'Invalid parameter "{k}" for {self.__class__.__name__}. Attribute does not exist.')
_Canvas.draw_queue.append(self)
@property
def fill_color(self):
'''The fill color of the shape'''
return self._fill_color
@fill_color.setter
def fill_color(self, v):
self._fill_color = v
@property
def fill_opacity(self):
'''The fill opacity of the shape'''
return self._fill_opacity
@fill_opacity.setter
def fill_opacity(self, v):
self._fill_opacity = v
@property
def close_path(self):
'''Upon rendering the shape, the last vertex will connect to the first vertex'''
return self._close_path
@close_path.setter
def close_path(self, v:bool):
self._close_path = v
@property
def stroke_color(self):
'''The stroke color of the shape'''
return self._stroke_color
@stroke_color.setter
def stroke_color(self, v):
self._stroke_color = v
@property
def stroke_width(self):
'''The stroke width of the shape'''
return self._stroke_width
@stroke_width.setter
def stroke_width(self, v):
self._stroke_width = v
@property
def vertices(self):
'''The list of vertices'''
return self._vertices
@property
def first_vertex(self):
'''The first vertex of the shape'''
rv = (None, None)
if self.vertices.size > 0:
_rv = self.vertices[0]
rv = (_rv[0], _rv[1])
return rv
@property
def min_x(self):
'''The smallest x value of all the vertices'''
rv = self.first_vertex[0]
_v = self.vertices.tolist()
for vertex in _v:
x = vertex[0]
if x < rv:
rv = x
return rv
@property
def max_x(self):
'''The largest x value of all the vertices'''
rv = self.first_vertex[0]
_v = self.vertices.tolist()
for vertex in _v:
x = vertex[0]
if x > rv:
rv = x
return rv
@property
def min_y(self):
'''The smallest y value of all the vertices'''
rv = self.first_vertex[1]
_v = self.vertices.tolist()
for vertex in _v:
y = vertex[1]
if y < rv:
rv = y
return rv
@property
def max_y(self):
'''The largest y value of all the vertices'''
rv = self.first_vertex[1]
_v = self.vertices.tolist()
for vertex in _v:
y = vertex[1]
if y > rv:
rv = y
return rv
@property
def center(self):
'''The center of the shape based on the min/max x/y'''
x = ((self.max_x - self.min_x)/2) + self.min_x
y = ((self.max_y - self.min_y)/2) + self.min_y
return (x,y)
def add_vertex(self, x, y, z=0):
'''Append a set of vertexes to the primitive shape'''
# vstack copies the existing array once; the old tolist()/np.array() round trip
# rebuilt every vertex through Python objects on each call
self._vertices = np.vstack([self._vertices.reshape(-1, 4), [[x, y, z, 1]]])
return self
def transform(self, x, y, z=0):
'''Transform the vertices based on x/y coords'''
translate_matrix = np.identity(4)
translate_matrix[0, -1] = x
translate_matrix[1, -1] = y
translate_matrix[2, -1] = z
self._vertices = np.dot(self._vertices, translate_matrix.T)[:, :4]
return self
def rotate(self, theta, origin=None, axis=np.array([0, 0, 1])):
"""Rotate the shape by the given angle along the given axis.
:param theta: The angle by which to rotate (in degrees)
:type theta: float
:param axis: The axis along which to rotate (defaults to the z-axis)
:type axis: np.ndarray or list
"""
# Convert degrees to radians
theta = theta * math.pi / 180
axis = np.array(axis[:])
x = axis[0]
y = axis[1]
z = axis[2]
s = np.sin(theta)
c = np.cos(theta)
c1 = 1 - c
rotation = np.identity(4)
rotation[0, 0] = x * x * c1 + c
rotation[0, 1] = x * y * c1 - z * s
rotation[0, 2] = x * z * c1 + y * s
rotation[1, 0] = y * x * c1 + z * s
rotation[1, 1] = y * y * c1 + c
rotation[1, 2] = y * z * c1 - x * s
rotation[2, 0] = x * z * c1 - y * s
rotation[2, 1] = y * z * c1 + x * s
rotation[2, 2] = z * z * c1 + c
if origin == None:
x_c,y_c = self.center
else:
x_c = origin[0]
y_c = origin[1]
self.transform(-x_c, -y_c)
rotation[0, -1] = x_c
rotation[1, -1] = y_c
rotation[2, -1] = 1
self._vertices = np.dot(self._vertices, rotation.T)[:, :4]
return self
def rotate_x(self, theta):
"""This is a convenience function to rotate the shape along the x axis.
:param theta: The angle by which to rotate (in degrees)
:type theta: float
:returns: The rotation matrix used to apply the transformation.
:rtype: np.ndarray
"""
self.rotate(theta, axis=np.array([1, 0, 0]))
return self
def rotate_y(self, theta):
"""This is a convenience function to rotate the shape along the y axis.
:param theta: The angle by which to rotate (in degrees)
:type theta: float
:returns: The rotation matrix used to apply the transformation.
:rtype: np.ndarray
"""
self.rotate(theta, axis=np.array([0, 1, 0]))
return self
def rotate_z(self, theta):
"""This is a convenience function to rotate the shape along the z axis.
:param theta:The angle by which to rotate (in degrees)
:type theta: float
:returns: The rotation matrix used to apply the transformation.
:rtype: np.ndarray
"""
self.rotate(theta, axis=np.array([0, 0, 1]))
return self
def scale(self, amt, amt_y=None, origin=None):
"""Scale the shape by the given amount.
:param amt: Scale factor for x-axis (2.0 = twice as big)
:type amt: float
:param amt_y: Scale factor for y-axis (defaults to amt for uniform scaling)
:type amt_y: float
:param origin: Origin point for scaling (defaults to shape center)
:type origin: tuple
"""
if amt_y == None:
amt_y = amt
scale_matrix = np.identity(4)
scale_matrix[0, 0] = amt
scale_matrix[1, 1] = amt_y
scale_matrix[2, 2] = 1 # default for z
self._vertices = self._vertices.dot(scale_matrix)
return self
@property
def svg_object(self):
if len(self.vertices) > 0:
verts = self.vertices
verts = verts.tolist()
# # verts = vertices.tolist()
if len(verts)== 0 :
print(f'verts contains one item of {[[0.0, 0.0, 0.0]]} ... continuing.')
return None
start_l = verts.pop(0)
start_x = start_l[0]
start_y = start_l[1]
other_verts = []
for o_v in verts:
x = o_v[0]
y = o_v[1]
# z = o_v[2]
other_verts.append(x)
other_verts.append(y)
return drawsvg.Lines(start_x, start_y, *other_verts, fill=self.fill_color, stroke=self.stroke_color,
stroke_width=self.stroke_width, fill_opacity=self.fill_opacity, close=self.close_path)
else:
raise Exception("This does not have vertices or a custom svg_object, so it cannot be rendered.")
All provided primitives inherit from this class. If a user wants to make their own primitive, its recommended to inherit from this class so the new primative is added to the draw queue.
View Source
def __init__(self,**kwargs):
self._vertices = np.array([])
self._fill_color='none'
self._stroke_color='black'
self._stroke_width=1
self._close_path = False
self._fill_opacity = 1.0
self.is_circle=False
self.is_arc=False
self.is_group = False
self.is_text = False
self.id = str(uuid.uuid4())
for k,v in kwargs.items():
if hasattr(self, k):
setattr(self, k, v)
else:
raise AttributeError(f'Invalid parameter "{k}" for {self.__class__.__name__}. Attribute does not exist.')
_Canvas.draw_queue.append(self)
The fill color of the shape
The fill opacity of the shape
Upon rendering the shape, the last vertex will connect to the first vertex
The stroke color of the shape
The stroke width of the shape
The list of vertices
The first vertex of the shape
The smallest x value of all the vertices
The largest x value of all the vertices
The smallest y value of all the vertices
The largest y value of all the vertices
The center of the shape based on the min/max x/y
View Source
def add_vertex(self, x, y, z=0):
'''Append a set of vertexes to the primitive shape'''
# vstack copies the existing array once; the old tolist()/np.array() round trip
# rebuilt every vertex through Python objects on each call
self._vertices = np.vstack([self._vertices.reshape(-1, 4), [[x, y, z, 1]]])
return self
Append a set of vertexes to the primitive shape
View Source
def transform(self, x, y, z=0):
'''Transform the vertices based on x/y coords'''
translate_matrix = np.identity(4)
translate_matrix[0, -1] = x
translate_matrix[1, -1] = y
translate_matrix[2, -1] = z
self._vertices = np.dot(self._vertices, translate_matrix.T)[:, :4]
return self
Transform the vertices based on x/y coords
View Source
def rotate(self, theta, origin=None, axis=np.array([0, 0, 1])):
"""Rotate the shape by the given angle along the given axis.
:param theta: The angle by which to rotate (in degrees)
:type theta: float
:param axis: The axis along which to rotate (defaults to the z-axis)
:type axis: np.ndarray or list
"""
# Convert degrees to radians
theta = theta * math.pi / 180
axis = np.array(axis[:])
x = axis[0]
y = axis[1]
z = axis[2]
s = np.sin(theta)
c = np.cos(theta)
c1 = 1 - c
rotation = np.identity(4)
rotation[0, 0] = x * x * c1 + c
rotation[0, 1] = x * y * c1 - z * s
rotation[0, 2] = x * z * c1 + y * s
rotation[1, 0] = y * x * c1 + z * s
rotation[1, 1] = y * y * c1 + c
rotation[1, 2] = y * z * c1 - x * s
rotation[2, 0] = x * z * c1 - y * s
rotation[2, 1] = y * z * c1 + x * s
rotation[2, 2] = z * z * c1 + c
if origin == None:
x_c,y_c = self.center
else:
x_c = origin[0]
y_c = origin[1]
self.transform(-x_c, -y_c)
rotation[0, -1] = x_c
rotation[1, -1] = y_c
rotation[2, -1] = 1
self._vertices = np.dot(self._vertices, rotation.T)[:, :4]
return self
Rotate the shape by the given angle along the given axis.
:param theta: The angle by which to rotate (in degrees) :type theta: float
:param axis: The axis along which to rotate (defaults to the z-axis) :type axis: np.ndarray or list
View Source
def rotate_x(self, theta):
"""This is a convenience function to rotate the shape along the x axis.
:param theta: The angle by which to rotate (in degrees)
:type theta: float
:returns: The rotation matrix used to apply the transformation.
:rtype: np.ndarray
"""
self.rotate(theta, axis=np.array([1, 0, 0]))
return self
This is a convenience function to rotate the shape along the x axis.
:param theta: The angle by which to rotate (in degrees) :type theta: float
:returns: The rotation matrix used to apply the transformation. :rtype: np.ndarray
View Source
def rotate_y(self, theta):
"""This is a convenience function to rotate the shape along the y axis.
:param theta: The angle by which to rotate (in degrees)
:type theta: float
:returns: The rotation matrix used to apply the transformation.
:rtype: np.ndarray
"""
self.rotate(theta, axis=np.array([0, 1, 0]))
return self
This is a convenience function to rotate the shape along the y axis.
:param theta: The angle by which to rotate (in degrees) :type theta: float
:returns: The rotation matrix used to apply the transformation. :rtype: np.ndarray
View Source
def rotate_z(self, theta):
"""This is a convenience function to rotate the shape along the z axis.
:param theta:The angle by which to rotate (in degrees)
:type theta: float
:returns: The rotation matrix used to apply the transformation.
:rtype: np.ndarray
"""
self.rotate(theta, axis=np.array([0, 0, 1]))
return self
This is a convenience function to rotate the shape along the z axis.
:param theta:The angle by which to rotate (in degrees) :type theta: float
:returns: The rotation matrix used to apply the transformation. :rtype: np.ndarray
View Source
def scale(self, amt, amt_y=None, origin=None):
"""Scale the shape by the given amount.
:param amt: Scale factor for x-axis (2.0 = twice as big)
:type amt: float
:param amt_y: Scale factor for y-axis (defaults to amt for uniform scaling)
:type amt_y: float
:param origin: Origin point for scaling (defaults to shape center)
:type origin: tuple
"""
if amt_y == None:
amt_y = amt
scale_matrix = np.identity(4)
scale_matrix[0, 0] = amt
scale_matrix[1, 1] = amt_y
scale_matrix[2, 2] = 1 # default for z
self._vertices = self._vertices.dot(scale_matrix)
return self
Scale the shape by the given amount.
:param amt: Scale factor for x-axis (2.0 = twice as big) :type amt: float :param amt_y: Scale factor for y-axis (defaults to amt for uniform scaling) :type amt_y: float :param origin: Origin point for scaling (defaults to shape center) :type origin: tuple
Canvas Singleton Object. This controls the output 'canvas', and rendering of the draw loop.
Attributes
- width
- height
View Source
class Text(BaseShape):
'''A block of single-stroke Hershey text, wrapped to `width`, with its top-left corner at (x, y).
See hershey_text.FONT_NAMES for the available fonts.'''
def __init__(self, text, x,y, width=200, font_size=18.0, align="left", line_spacing=1.0, font_name='futural', **kwargs):
''''''
super().__init__(**kwargs)
self.x = x
self.y = y
self.text = text
self.is_text=True
# a list of complex arrays (x + iy), one per pen stroke; the canvas renders these directly
self.text_lines = text_block(text, width, font_name, font_size, align, line_spacing)
self.transform(x, y)
def add_vertex(self, *coords):
raise Exception("Cannot accept additional vertexes")
def transform(self, x, y, z=0):
c = complex(x, y)
for line in self.text_lines:
line += c
return self
A block of single-stroke Hershey text, wrapped to width, with its top-left corner at (x, y).
See hershey_text.FONT_NAMES for the available fonts.
View Source
def __init__(self, text, x,y, width=200, font_size=18.0, align="left", line_spacing=1.0, font_name='futural', **kwargs):
''''''
super().__init__(**kwargs)
self.x = x
self.y = y
self.text = text
self.is_text=True
# a list of complex arrays (x + iy), one per pen stroke; the canvas renders these directly
self.text_lines = text_block(text, width, font_name, font_size, align, line_spacing)
self.transform(x, y)
View Source
def add_vertex(self, *coords):
raise Exception("Cannot accept additional vertexes")
Append a set of vertexes to the primitive shape
View Source
def transform(self, x, y, z=0):
c = complex(x, y)
for line in self.text_lines:
line += c
return self
Transform the vertices based on x/y coords