"""Various types of points for vertices etc."""
import math
from copy import copy
import pyx
from pyfeyn2.render.pyx import config
from pyfeyn2.render.pyx.deco import PointLabel
from pyfeyn2.render.pyx.diagrams import FeynDiagram
from pyfeyn2.render.pyx.paint import BLACK
from pyfeyn2.render.pyx.utils import Visible
[docs]def midpoint(point1, point2):
"Return the point midway between this point and the argument."
return Point(
(point1.getX() + point2.getX()) / 2.0, (point1.getY() + point2.getY()) / 2.0
)
[docs]def distance(point1, point2):
"Calculate the distance between this point and the argument."
return math.hypot(point1.x() - point2.x(), point1.y() - point2.y())
## Point base class
[docs]class Point:
"""Base class for all pointlike objects in Feynman diagrams."""
[docs] def __init__(self, x, y, blob=None, labels=None, **kwargs):
"""Constructor."""
if labels is None:
labels = []
self.setXY(x, y)
self.setBlob(blob)
self.labels = labels
[docs] def addLabel(self, text, displace=0.3, angle=0, size=pyx.text.size.normalsize):
"""Add a LaTeX label to this point, either via parameters or actually as
a PointLable object."""
if text is None:
return self
if config.getOptions().DEBUG:
print("Adding label: " + text)
self.labels.append(
PointLabel(text=text, point=self, displace=displace, angle=angle, size=size)
)
if config.getOptions().DEBUG:
print("Labels = " + str(self.labels))
return self
[docs] def removeLabels(self):
"""Remove all labels from this point."""
self.labels = []
return self
[docs] def draw(self, canvas):
"Do nothing (abstract base class)."
pass
[docs] def getPath(self):
"Return the path of the attached blob path, if there is one, otherwise None."
if self.getBlob() and hasattr(self.getBlob(), "getPath"):
return self.getBlob().getPath()
else:
return None
[docs] def midpoint(self, otherpoint):
"Return the point midway between this point and the argument."
return midpoint(self, otherpoint)
[docs] def distance(self, otherpoint):
"Calculate the distance between this point and the argument."
return distance(self, otherpoint)
[docs] def intercept(self, otherpoint):
"Return the y-intercept of the straight line defined by this point and the argument."
return self.y() - self.tangent(otherpoint) * self.x()
[docs] def tangent(self, otherpoint):
"Return the tangent of the straight line defined by this point and the argument."
if otherpoint.x() != self.x():
return (otherpoint.y() - self.y()) / (otherpoint.x() - self.x())
else:
return float(10000) ## An arbitrary large number to replace infinity
[docs] def arg(self, otherpoint):
"""Return the angle between the x-axis and the straight line defined
by this point and the argument (cf. complex numbers)."""
arg = None
if otherpoint.x() == self.x():
if otherpoint.y() > self.y():
arg = math.pi / 2.0
elif otherpoint.y() < self.y():
arg = (
3 * math.pi / 2.0
) # this will be reset to 0 if the points are the same
if otherpoint.y() == self.y():
if otherpoint.x() < self.x():
arg = math.pi
else:
arg = 0.0
if otherpoint.x() != self.x() and otherpoint.y() != self.y():
arg = math.atan((otherpoint.y() - self.y()) / (otherpoint.x() - self.x()))
if otherpoint.x() < self.x():
arg += math.pi
elif otherpoint.y() < self.y():
arg += 2 * math.pi
## Convert to degrees
argindegs = math.degrees(arg)
return argindegs
[docs] def getBlob(self):
"Get the attached blob."
return self.blob
[docs] def setBlob(self, blob):
"Set the attached blob."
self.blob = blob
return self
[docs] def getX(self):
"Return the x-coordinate of this point."
return self.xpos
[docs] def setX(self, x):
"Set the x-coordinate of this point."
self.xpos = x
return self
[docs] def getY(self):
"Return the y-coordinate of this point."
return self.ypos
[docs] def setY(self, y):
"Set the y-coordinate of this point."
self.ypos = y
return self
[docs] def getXY(self):
"Return the x and y coordinates of this point as a 2-tuple."
return self.getX(), self.getY()
[docs] def setXY(self, xpos, ypos):
"Set the x and y coordinates of this point."
if xpos is None or ypos is None:
raise Exception("Point coordinates cannot be None")
self.setX(float(xpos))
self.setY(float(ypos))
return self
[docs] def x(self):
"Alias for getX()."
return self.getX()
[docs] def y(self):
"Alias for getY()."
return self.getY()
[docs] def xy(self):
"Alias for getXY()."
return self.getXY()
## Decorated point class
[docs]class DecoratedPoint(Point, Visible):
"Class for a point drawn with a marker"
[docs] def __init__(
self,
x=None,
y=None,
center=None,
mark=None,
fill=[BLACK],
stroke=[BLACK],
blob=None,
labels=[],
**kwargs
):
"""Constructor."""
xx = 0
yy = 0
if x is not None and y is not None:
xx = x
yy = y
elif center is not None:
xx = center.getX()
yy = center.getY()
else:
raise Exception("No (x,y) or no center specified for blob.")
Point.__init__(self, xx, yy, blob, labels)
self.setMark(copy(mark))
self.layeroffset = 1000
self.fillstyles = copy(fill) # lists are mutable --
self.strokestyles = copy(stroke) # hence make a copy!
## Add this to the current diagram automatically
FeynDiagram.currentDiagram.add(self)
[docs] def getPath(self):
"""Return the path belonging to the blob or marker attached to this point, if any."""
if self.blob:
return self.getBlob().getPath()
elif self.marker:
return self.getMark().getPath()
else:
return None
[docs] def getMark(self):
"""Return the marker attached to this point."""
return self.marker
[docs] def setMark(self, mark):
"""Set the marker attached to this point."""
self.marker = mark
if self.marker is not None:
self.marker.setPoint(self)
# if size is None and self.radius == 0: # change shape of a true point?
# self.radius = 4*unit.t_pt # probably want to use default size
return self
[docs] def getBlob(self):
"""Return the blob attached to this point."""
return self.blob
[docs] def setBlob(self, blob):
"""Set the blob attached to this point."""
self.blob = blob
if self.blob is not None:
self.blob.setPoints([self])
return self
[docs] def getFillstyles(self):
"""Return the fillstyles for the marker or blob attached to this point."""
return self.fillstyles
[docs] def setFillstyles(self, styles):
"""Set the fillstyles for the marker or blob attached to this point."""
self.fillstyles = styles
return self
[docs] def addFillstyles(self, styles):
"""Add fillstyles to the marker or blob attached to this point."""
if styles is not None:
self.fillstyles.add(styles)
return self
[docs] def addFillstyle(self, style):
"""Add a fillstyle to the marker or blob attached to this point."""
self.fillstyles.append(style)
return self
[docs] def getStrokestyles(self):
"""Return the stroke styles for the marker or blob attached to this point."""
return self.strokestyles
[docs] def setStrokestyles(self, styles):
"""Set the stroke styles for the marker or blob attached to this point."""
self.strokestyles = styles
return self
[docs] def addStrokestyles(self, styles):
"""Add stroke styles to the marker or blob attached to this point."""
self.strokestyles.add(styles)
return self
[docs] def addStrokestyle(self, style):
"""Add a stroke style to the marker or blob attached to this point."""
self.strokestyles.append(style)
return self
[docs] def draw(self, canvas):
"""Draw the marker or blob attached to this point."""
if self.getPath():
canvas.fill(self.getPath(), self.fillstyles)
canvas.stroke(self.getPath(), self.strokestyles)
for l in self.labels:
l.draw(canvas)
## Vertex is an alias for DecoratedPoint
Vertex = DecoratedPoint
[docs]class Mark:
[docs] def getPoint(self):
"""Return the point to which this marker is attached."""
return self.point
[docs] def setPoint(self, point):
"""Attach this marker to a new point."""
self.point = point
return self
[docs]class CircleMark(Mark):
[docs] def __init__(self, size=0.075):
"""A circular mark."""
self.radius = size
self.point = None
[docs] def getPath(self):
"""Return the path for this marker."""
if self.point is not None:
x, y = self.point.getXY()
return pyx.path.circle(x, y, self.radius).path()
return None
[docs]class PolygonalMark(Mark):
[docs] def __init__(self, size=0.075, corners=3):
"""A polygonal mark."""
self.radius = size
self.n = corners
self.point = None
[docs] def getPath(self):
"""Return the path for this marker."""
if self.point is not None:
x, y = self.point.getXY()
return pyx.box.polygon(
[
(
x - self.radius * math.sin(i * 2 * math.pi / self.n),
y + self.radius * math.cos(i * 2 * math.pi / self.n),
)
for i in range(self.n)
]
).path()
return None
[docs]class StarshapeMark(Mark):
[docs] def __init__(self, size=0.075, raysize=0.05, rays=3):
"""A star-shaped mark."""
self.radius = size
self.wiggle = raysize
self.n = rays
self.point = None
[docs] def getPath(self):
"""Return the path for this marker."""
if self.point is not None:
x, y = self.point.getXY()
return pyx.box.polygon(
[
(
x
- (self.radius - self.wiggle * (i % 2))
* math.sin(i * math.pi / self.n),
y
+ (self.radius - self.wiggle * (i % 2))
* math.cos(i * math.pi / self.n),
)
for i in range(2 * self.n)
]
).path()
return None
[docs]class CrossMark(Mark):
[docs] def __init__(self, size=0.075):
"""A cross marker, e.g. to show neutrino oscillations."""
self.size = size
self.point = None
[docs] def getPath(self):
"""Return the path for this marker."""
if self.point is not None:
x, y = self.point.getXY()
return pyx.path.path(
pyx.path.moveto(x - self.size, y - self.size),
pyx.path.lineto(x + self.size, y + self.size),
pyx.path.moveto(x - self.size, y + self.size),
pyx.path.lineto(x + self.size, y - self.size),
).path()
return None
## Convenience constants
CIRCLE = CircleMark()
SQUARE = SquareMark()
CROSS = CrossMark()
TRIANGLE = PolygonalMark(corners=3)
DIAMOND = PolygonalMark(corners=4)
PENTAGON = PolygonalMark(corners=5)
HEXAGON = PolygonalMark(corners=6)
HEPTAGON = PolygonalMark(corners=7)
OCTAGON = PolygonalMark(corners=8)
TETRASTAR = StarshapeMark(rays=4)
STAR = StarshapeMark(rays=5)
HEXASTAR = StarshapeMark(rays=6)
OCTOSTAR = StarshapeMark(rays=8)
NamedMark = {"none": lambda size: None, "circle": CircleMark, "square": SquareMark}