Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clip! Clip! Clip! #8

Merged
merged 6 commits into from
Oct 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/classes/Canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,5 @@ def add_def(self, other: Union[Font, Shape, Text]):
elif isinstance(other, Text):
# get font from text and add to defs
self.defs_map[other.font.family] = other.font.defs()
elif isinstance(other, Shape) and other.clipped == True:
elif isinstance(other, Shape) and other.clip != None:
self.defs_map[id(other)] = other.defs()
9 changes: 9 additions & 0 deletions src/classes/Clip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Clip(object):
def __init__(self, clipped_in : bool=True):
self.clipped_in = clipped_in

def is_inner(self):
return self.clipped_in

def is_outer(self):
return not self.clipped_in
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a new line to the end of the file

9 changes: 6 additions & 3 deletions src/classes/Item.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ def add_child(self, other) -> None:
def render(self) -> str:
raise NotImplementedError

def render_children(self) -> str:
def render_children(self, mask=False) -> str:
# Start group for children
s = ('\t' * self.depth) + f'<g transform="translate({self.position.x}, {self.position.y})"'
# Check if clipped
if self.clipped == True:
s += f' style="clip-path: url(#{id(self)});"'
if self.clip != None:
if not mask:
s += f' style="clip-path: url(#{id(self)});"'
else:
s += f' style="mask: url(#{id(self)});"'
s += '>\n'
for child in self.children:
s += ('\t' * (self.depth + 1)) + child.render() + '\n'
Expand Down
52 changes: 28 additions & 24 deletions src/classes/parser/Generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from ..Fill import Fill
from ..Stroke import Stroke
from ..Clip import Clip

from ..parser.Expressions import *

Expand Down Expand Up @@ -152,14 +153,17 @@ def rotate_from_tree_modifier(tree, defs):
return 0

@staticmethod
def clipped_from_tree_modifier(tree, defs):
def clip_from_tree_modifier(tree, defs):
for modifier in tree.children[0].children:
M = modifier.children[0]
Mname = M.data if type(M) == Tree else M.value
if type(M) == Token:
if type(M) == Tree:
if Mname == "clipped":
return True
return False
if len(M.children) > 0 and M.children[0].children[0].value == "out":
return Clip(False)
else:
return Clip(True)
return None

@staticmethod
def outline_from_tree_modifier(tree, defs):
Expand Down Expand Up @@ -228,22 +232,22 @@ def exception(line, col, msg):


class RectGenerator(Generator):
def __init__(self, x, y, width_expr, height_expr, fill=None, stroke=None, rotate=0, clipped=False, children=[]):
def __init__(self, x, y, width_expr, height_expr, fill=None, stroke=None, rotate=0, clip=None, children=[]):
self.x, self.y = x, y
self.width_expr = width_expr
self.height_expr = height_expr
self.fill = fill
self.stroke = stroke
self.rotate = rotate
self.clipped = clipped
self.clip = clip
self.children = children

def generate(self, pw, ph):
width = self.width_expr.eval(pw, ph)
height = self.height_expr.eval(pw, ph)
pos = Vector2(self.x.eval(pw, ph), self.y.eval(pw, ph))
children = self.generate_children(width, height)
return Rect(self.clipped, pos, width, height, self.stroke, self.fill, children, self.rotate)
return Rect(self.clip, pos, width, height, self.stroke, self.fill, children, self.rotate)

@staticmethod
def from_parse_tree(tree, defs):
Expand All @@ -260,27 +264,27 @@ def from_parse_tree(tree, defs):
Generator.validate_modifiers(tree, ("rotated", "clipped", "outlined"))

rotate = Generator.rotate_from_tree_modifier(tree, defs)
clipped = Generator.clipped_from_tree_modifier(tree, defs)
clip = Generator.clip_from_tree_modifier(tree, defs)
stroke = Generator.outline_from_tree_modifier(tree, defs)

return RectGenerator(x, y, width, height, fill, stroke, rotate, clipped, children)
return RectGenerator(x, y, width, height, fill, stroke, rotate, clip, children)


class CircleGenerator(Generator):
def __init__(self, x, y, radius_expr, fill=None, stroke=None, rotate=0, clipped=False, children=[]):
def __init__(self, x, y, radius_expr, fill=None, stroke=None, rotate=0, clip=None, children=[]):
self.x, self.y = x, y
self.radius_expr = radius_expr
self.fill = fill
self.stroke = stroke
self.rotate = rotate
self.clipped = clipped
self.clip = clip
self.children = children

def generate(self, pw, ph):
radius = self.radius_expr.eval(pw, ph)
pos = Vector2(self.x.eval(pw, ph), self.y.eval(pw, ph))
children = self.generate_children(radius*2, radius*2)
return Circle(self.clipped, pos, radius, self.stroke, self.fill, children, self.rotate)
return Circle(self.clip, pos, radius, self.stroke, self.fill, children, self.rotate)

@staticmethod
def from_parse_tree(tree, defs):
Expand All @@ -295,28 +299,28 @@ def from_parse_tree(tree, defs):
Generator.validate_modifiers(tree, ("rotated", "clipped", "outlined"))

rotate = Generator.rotate_from_tree_modifier(tree, defs)
clipped = Generator.clipped_from_tree_modifier(tree, defs)
clip = Generator.clip_from_tree_modifier(tree, defs)
stroke = Generator.outline_from_tree_modifier(tree, defs)

return CircleGenerator(x, y, rad, fill, stroke, rotate, clipped, children)
return CircleGenerator(x, y, rad, fill, stroke, rotate, clip, children)

class EllipseGenerator(Generator):
def __init__(self, x, y, xrad_expr, yrad_expr, fill=None, stroke=None, rotate=0, clipped=False, children=[]):
def __init__(self, x, y, xrad_expr, yrad_expr, fill=None, stroke=None, rotate=0, clip=None, children=[]):
self.x, self.y = x, y
self.xrad_expr = xrad_expr
self.yrad_expr = yrad_expr
self.fill = fill
self.stroke = stroke
self.rotate = rotate
self.clipped = clipped
self.clip = clip
self.children = children

def generate(self, pw, ph):
xrad = self.xrad_expr.eval(pw, ph)
yrad = self.yrad_expr.eval(pw, ph)
pos = Vector2(self.x.eval(pw, ph), self.y.eval(pw, ph))
children = self.generate_children(xrad*2, yrad*2)
return Ellipse(self.clipped, pos, xrad, yrad, self.stroke, self.fill, children, self.rotate)
return Ellipse(self.clip, pos, xrad, yrad, self.stroke, self.fill, children, self.rotate)

@staticmethod
def from_parse_tree(tree, defs):
Expand All @@ -332,10 +336,10 @@ def from_parse_tree(tree, defs):
Generator.validate_modifiers(tree, ("rotated", "clipped", "outlined"))

rotate = Generator.rotate_from_tree_modifier(tree, defs)
clipped = Generator.clipped_from_tree_modifier(tree, defs)
clip = Generator.clip_from_tree_modifier(tree, defs)
stroke = Generator.outline_from_tree_modifier(tree, defs)

return EllipseGenerator(x, y, xrad, yrad, fill, stroke, rotate, clipped, children)
return EllipseGenerator(x, y, xrad, yrad, fill, stroke, rotate, clip, children)

class LineGenerator(Generator):
def __init__(self, x1, y1, x2, y2, stroke=None):
Expand All @@ -360,20 +364,20 @@ def from_parse_tree(tree, defs):
return LineGenerator(x1, y1, x2, y2, stroke)

class PolygonGenerator(Generator):
def __init__(self, points, fill=None, stroke=None, rotate=0, clipped=False, children=[]):
def __init__(self, points, fill=None, stroke=None, rotate=0, clip=None, children=[]):
self.points = points
self.fill = fill
self.stroke = stroke
self.rotate = rotate
self.clipped = clipped
self.clip = clip
self.children = children

def generate(self, pw, ph):
points = [Vector2(P[0].eval(pw, ph), P[1].eval(pw, ph)) for P in self.points]
xs = [p.x for p in points]
ys = [p.y for p in points]
children = self.generate_children(abs(min(xs)-max(xs)), abs(min(ys)-max(ys)))
return Polygon(self.clipped, points, self.stroke, self.fill, children, self.rotate)
return Polygon(self.clip, points, self.stroke, self.fill, children, self.rotate)

@staticmethod
def from_parse_tree(tree, defs):
Expand All @@ -388,10 +392,10 @@ def from_parse_tree(tree, defs):
Generator.validate_modifiers(tree, ("rotated", "clipped", "outlined"))

rotate = Generator.rotate_from_tree_modifier(tree, defs)
clipped = Generator.clipped_from_tree_modifier(tree, defs)
clip = Generator.clip_from_tree_modifier(tree, defs)
stroke = Generator.outline_from_tree_modifier(tree, defs)

return PolygonGenerator(points, fill, stroke, rotate, clipped, children)
return PolygonGenerator(points, fill, stroke, rotate, clip, children)

class ImageGenerator(Generator):
def __init__(self, x, y, width_expr, height_expr=None, path="", rotate=0):
Expand Down
13 changes: 10 additions & 3 deletions src/classes/primitives/Circle.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Union, Optional, Iterable
from ..Stroke import Stroke
from ..Fill import Fill
from ..Clip import Clip
from ..Item import Item
from .Shape import Shape
from ..util.Vector2 import Vector2
Expand All @@ -12,12 +13,12 @@ class Circle(Shape):
"""
# create circle as an ellipse with same x and y radius
def __init__(self,
clipped: bool, position: Vector2,
clip: Clip, position: Vector2,
radius: Union[int, float],
stroke: Optional[Stroke] = None, fill: Optional[Fill] = None,
children: Iterable[Item] = [], rotation: Union[int, float] = 0
):
super().__init__(clipped, position,
super().__init__(clip, position,
stroke=stroke, fill=fill,
children=children, rotation=rotation)
self.radius: Union[int, float] = radius
Expand All @@ -33,6 +34,8 @@ def render(self) -> str:
s += ' ' + self.stroke.render()
if self.fill != None:
s += ' ' + self.fill.render()
else:
s += ' ' + 'fill="#000000" fill-opacity="0.0"'
s += ' />'
# Render Children
if len(self.children) > 0:
Expand All @@ -43,7 +46,11 @@ def render(self) -> str:
def defs(self) -> str:
# Create defs
s = super().defs()
s += f'<circle cx="-{self.position.x}" cy="-{self.position.y}" r="{self.radius}"'
stroke_width = 0 if self.stroke == None else self.stroke.width
if self.clip != None and self.clip.is_inner():
s += f'<circle cx="0" cy="0" r="{self.radius}"'
elif self.clip != None and self.clip.is_outer():
s += f'<circle cx="0" cy="0" r="{self.radius+stroke_width//2+1}"'
# Apply rotation if needed
if abs(self.rotation) > 1e-6:
s += f' transform="rotate({self.rotation})"'
Expand Down
13 changes: 10 additions & 3 deletions src/classes/primitives/Ellipse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from ..Stroke import Stroke
from ..Fill import Fill
from ..Item import Item
from ..Clip import Clip
from .Shape import Shape
from ..util.Vector2 import Vector2

Expand All @@ -14,12 +15,12 @@ class Ellipse(Shape):
# create circle as an ellipse with same x and y radius

def __init__(self,
clipped: bool, position: Vector2,
clip: Clip, position: Vector2,
rx: Union[int, float], ry: Union[int, float],
stroke: Optional[Stroke] = None, fill: Optional[Fill] = None,
children: Iterable[Item] = [], rotation: Union[int, float] = 0
):
super().__init__(clipped, position,
super().__init__(clip, position,
stroke=stroke, fill=fill,
children=children, rotation=rotation)
self.rx: Union[int, float] = rx
Expand All @@ -36,6 +37,8 @@ def render(self) -> str:
s += ' ' + self.stroke.render()
if self.fill != None:
s += ' ' + self.fill.render()
else:
s += ' ' + 'fill="#000000" fill-opacity="0.0"'
s += ' />'
# Render Children
if len(self.children) > 0:
Expand All @@ -46,7 +49,11 @@ def render(self) -> str:
def defs(self) -> str:
# Create defs
s = super().defs()
s += f'<ellipse cx="-{self.position.x}" cy="-{self.position.y}" rx="{self.rx}" ry="{self.ry}"'
stroke_width = 0 if self.stroke == None else self.stroke.width
if self.clip != None and self.clip.is_inner():
s += f'<ellipse cx="0" cy="0" rx="{self.rx}" ry="{self.ry}"'
elif self.clip != None and self.clip.is_outer():
s += f'<ellipse cx="0" cy="0" rx="{self.rx+stroke_width//2+1}" ry="{self.ry+stroke_width//2+1}"'
# Apply rotation if needed
if abs(self.rotation) > 1e-6:
s += f' transform="rotate({self.rotation})"'
Expand Down
5 changes: 3 additions & 2 deletions src/classes/primitives/Line.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
from ..Stroke import Stroke
from ..Fill import Fill
from ..Item import Item
from ..Clip import Clip
from .Shape import Shape
from ..util.Vector2 import Vector2

class Line(Shape):
def __init__(self,
clipped: bool,
clip: Clip,
start: Vector2, end: Vector2,
stroke: Stroke,
children: Iterable[Item] = [], rotation: Union[int, float] = 0
):
position = (end - start) * 2 + start
super().__init__(clipped, position,
super().__init__(clip, position,
stroke=stroke, fill=None,
children=children, rotation=rotation)
self.start: Vector2 = start
Expand Down
8 changes: 6 additions & 2 deletions src/classes/primitives/Polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from ..Stroke import Stroke
from ..Fill import Fill
from ..Item import Item
from ..Clip import Clip
from .Shape import Shape
from ..util.Vector2 import Vector2

Expand All @@ -14,7 +15,7 @@ class Polygon(Shape):
# create circle as an ellipse with same x and y radius

def __init__(self,
clipped: bool,
clip: Clip,
points: Iterable[Vector2],
stroke: Optional[Stroke] = None, fill: Optional[Fill] = None,
children: Iterable[Item] = [], rotation: Union[int, float] = 0
Expand All @@ -27,7 +28,7 @@ def __init__(self,
center.x /= len(points)
center.y /= len(points)
# set position to center
super().__init__(clipped, center,
super().__init__(clip, center,
stroke=stroke, fill=fill,
children=children, rotation=rotation)
self.points = points
Expand All @@ -50,6 +51,8 @@ def render(self) -> str:
s += ' ' + self.stroke.render()
if self.fill != None:
s += ' ' + self.fill.render()
else:
s += ' ' + 'fill="#000000" fill-opacity="0.0"'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe for such defaults we should have a defaults.py file with a bunch of enums and default values. So, if we decide to change something later, it will be much easier to hunt down all the cases where it was used. For example if we want to change the default color.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a separate PR though

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sounds like a good idea for other default values. I only put black in there because it needed a fill color. All this does is make the fill invisible when PhoTeX is supplied "no fill" for a shape!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but what if in the future we want the default color to be customization or we want it to be white? We would need to go through all the files and manually update them. My idea was to have a unified location for such defaults. Again though, let's keep this separate from this PR.

Also, I think transparent black is a good choice.

s += ' />'
# Render Children
if len(self.children) > 0:
Expand All @@ -66,6 +69,7 @@ def defs(self) -> str:
if index != len(self.points) - 1:
s += ' '
s += '" '

# Apply translation for map since it is relative to parent:
s += f' transform="translate(-{self.position.x}, -{self.position.y})"'
# Apply rotation if needed
Expand Down