Skip to content

Instantly share code, notes, and snippets.

@clayote
Last active August 29, 2015 13:56
Show Gist options
  • Save clayote/9009935 to your computer and use it in GitHub Desktop.
Save clayote/9009935 to your computer and use it in GitHub Desktop.
Arrow class connecting one node to another. The nodes may not be the same size, and may move arbitrarily.
# This file is part of LiSE, a framework for life simulation games.
# Copyright (c) 2013 Zachary Spector, zacharyspector@gmail.com
"""That which displays a one-way connection between two places.
An arrow connects two spots, the origin and the destination, and it
points from the origin to the destination, regardless of where on the
screen they are at the moment.
"""
from math import hypot, atan2, pi, sin, cos
from LiSE.util import (
fortyfive,
ninety
)
from LiSE.gui.kivybits import LiSEWidgetMetaclass
from kivy.uix.widget import Widget
from kivy.properties import (
ObjectProperty,
ListProperty,
ReferenceListProperty,
BoundedNumericProperty,
)
from kivy.clock import Clock
def slope_theta_rise_run(rise, run):
"""Return a radian value expressing the angle at the lower-left corner
of a triangle ``rise`` high, ``run`` wide.
If ``run`` is zero, but ``rise`` is positive, return pi / 2. If
``run`` is zero, but ``rise`` is negative, return -pi / 2.
"""
try:
return atan2(rise, run)
except ZeroDivisionError:
if rise >= 0:
return ninety
else:
return -1 * ninety
def truncated_line(leftx, boty, rightx, topy, r, from_start=False):
"""Return coordinates for two points, very much like the two points
supplied, but with the end of the line foreshortened by amount r.
"""
# presumes pointed up and right
if r == 0:
return (leftx, boty, rightx, topy)
rise = topy - boty
run = rightx - leftx
length = hypot(rise, run) - r
theta = slope_theta_rise_run(rise, run)
if from_start:
leftx = rightx - cos(theta) * length
boty = topy - sin(theta) * length
else:
rightx = leftx + cos(theta) * length
topy = boty + sin(theta) * length
return (leftx, boty, rightx, topy)
def opp_theta_rise_run(rise, run):
"""Inverse of ``slope_theta_rise_run``"""
try:
return atan2(run, rise)
except ZeroDivisionError:
if run >= 0:
return ninety
else:
return -1 * ninety
def wedge_offsets_core(theta, opp_theta, taillen):
"""Internal use"""
top_theta = theta - fortyfive
bot_theta = pi - fortyfive - opp_theta
xoff1 = cos(top_theta) * taillen
yoff1 = sin(top_theta) * taillen
xoff2 = cos(bot_theta) * taillen
yoff2 = sin(bot_theta) * taillen
return (
xoff1, yoff1, xoff2, yoff2)
def wedge_offsets_rise_run(rise, run, taillen):
"""Given a line segment's rise, run, and length, return two new
points--with respect to the *end* of the line segment--that are good
for making an arrowhead with.
The arrowhead is a triangle formed from these points and the point at
the end of the line segment.
"""
# theta is the slope of a line bisecting the ninety degree wedge.
theta = slope_theta_rise_run(rise, run)
opp_theta = opp_theta_rise_run(rise, run)
return wedge_offsets_core(theta, opp_theta, taillen)
def get_arrow_points(ox, orx, oy, ory, dx, drx, dy, dry, taillen):
ox += orx
oy += ory
dx += drx
dy += dry
if drx > dry:
dr = drx
else:
dr = dry
if dy < oy:
yco = -1
else:
yco = 1
if dx < ox:
xco = -1
else:
xco = 1
(leftx, boty, rightx, topy) = truncated_line(
float(ox * xco), float(oy * yco),
float(dx * xco), float(dy * yco),
dr + 1)
rise = topy - boty
run = rightx - leftx
if rise == 0:
xoff1 = cos(fortyfive) * taillen
yoff1 = xoff1
xoff2 = xoff1
yoff2 = -1 * yoff1
elif run == 0:
xoff1 = sin(fortyfive) * taillen
yoff1 = xoff1
xoff2 = -1 * xoff1
yoff2 = yoff1
else:
(xoff1, yoff1, xoff2, yoff2) = wedge_offsets_rise_run(
rise, run, taillen)
x1 = (rightx - xoff1) * xco
x2 = (rightx - xoff2) * xco
y1 = (topy - yoff1) * yco
y2 = (topy - yoff2) * yco
endx = rightx * xco
endy = topy * yco
r = [ox, oy,
endx, endy, x1, y1,
endx, endy, x2, y2,
endx, endy]
return r
class Arrow(Widget):
"""A widget that points from one :class:`~LiSE.gui.board.Spot` to
another.
:class:`Arrow`s are the graphical representations of
:class:`~LiSE.model.Portal`s. They point from the :class:`Spot`
representing the :class:`Portal`'s origin, to the one representing
its destination.
"""
__metaclass__ = LiSEWidgetMetaclass
kv = """
<Arrow>:
canvas:
Color:
rgba: root.bg_color
Line:
width: root.w * 1.4
points: root.points
Color:
rgba: root.fg_color
Line:
width: root.w
points: root.points
"""
margin = 10
"""When deciding whether a touch collides with me, how far away can
the touch get before I should consider it a miss?"""
w = 1
"""The width of the inner, brighter portion of the :class:`Arrow`. The
whole :class:`Arrow` will end up thicker."""
board = ObjectProperty()
"""The board on which I am displayed."""
portal = ObjectProperty()
"""The portal that I represent."""
"""Pawns that are part-way through me. Each needs to present a
'progress' property to let me know how far through me they ought to be
repositioned."""
pawns_here = ListProperty([])
bg_r = BoundedNumericProperty(0.25, min=0., max=1.)
bg_g = BoundedNumericProperty(0.25, min=0., max=1.)
bg_b = BoundedNumericProperty(0.25, min=0., max=1.)
bg_a = BoundedNumericProperty(0.8, min=0., max=1.)
bg_color = ReferenceListProperty(bg_r, bg_g, bg_b, bg_a)
fg_r = BoundedNumericProperty(1, min=0., max=1.)
fg_g = BoundedNumericProperty(1, min=0., max=1.)
fg_b = BoundedNumericProperty(1, min=0., max=1.)
fg_a = BoundedNumericProperty(1, min=0., max=1.)
fg_color = ReferenceListProperty(fg_r, fg_g, fg_b, fg_a)
points = ListProperty()
def __init__(self, **kwargs):
"""Bind some properties, and put the relevant instructions into the
canvas--but don't put any point data into the instructions
just yet. For that, wait until ``on_parent``, when we are
guaranteed to know the positions of our endpoints.
"""
self.trigger_repoint = Clock.create_trigger(
self.repoint, timeout=-1)
self.trigger_repawn = Clock.create_trigger(
self.repawn, timeout=-1)
Widget.__init__(self, **kwargs)
self.board.arrowdict[unicode(self.portal)] = self
orign = unicode(self.portal.origin)
destn = unicode(self.portal.destination)
self.board.spotdict[orign].bind(
pos=self.trigger_repoint,
size=self.trigger_repoint)
self.board.spotdict[destn].bind(
pos=self.trigger_repoint,
size=self.trigger_repoint)
self.board.host.closet.register_time_listener(
self.trigger_repawn)
def __unicode__(self):
"""Return Unicode name of my :class:`Portal`"""
return unicode(self.portal)
def __str__(self):
"""Return string name of my :class:`Portal`"""
return str(self.portal)
@property
def reciprocal(self):
"""If it exists, return the edge of the :class:`Portal` that connects
the same two places that I do, but in the opposite
direction. Otherwise, return ``None``.
"""
# Return the edge of the portal that connects the same two
# places in the opposite direction, supposing it exists
try:
return self.portal.reciprocal.arrow
except KeyError:
return None
def handle_time(self, b, t):
pass
def get_points(self):
"""Return the coordinates of the points that describe my shape."""
orig = self.board.spotdict[unicode(self.portal.origin)]
dest = self.board.spotdict[unicode(self.portal.destination)]
(ox, oy) = orig.pos
try:
(ow, oh) = orig.size
except AttributeError:
(ow, oh) = (0, 0)
taillen = float(self.board.arrowhead_size)
orx = ow / 2
ory = ow / 2
(dx, dy) = dest.pos
try:
(dw, dh) = dest.size
except AttributeError:
(dw, dh) = (0, 0)
drx = dw / 2
dry = dh / 2
return get_arrow_points(ox, orx, oy, ory, dx, drx, dy, dry, taillen)
def repoint(self, *args):
self.points = self.get_points()
def get_slope(self):
"""Return a float of the increase in y divided by the increase in x,
both from left to right."""
orig = self.board.spotdict[unicode(self.portal.origin)]
dest = self.board.spotdict[unicode(self.portal.destination)]
ox = orig.x
oy = orig.y
dx = dest.x
dy = dest.y
if oy == dy:
return 0
elif ox == dx:
return None
else:
rise = dy - oy
run = dx - ox
return rise / run
def get_b(self):
"""Return my Y-intercept.
I probably don't really hit the left edge of the window, but
this is where I would, if I were long enough.
"""
orig = self.board.spotdict[unicode(self.portal.origin)]
dest = self.board.spotdict[unicode(self.portal.destination)]
(ox, oy) = orig.pos
(dx, dy) = dest.pos
denominator = dx - ox
x_numerator = (dy - oy) * ox
y_numerator = denominator * oy
return ((y_numerator - x_numerator), denominator)
def collide_point(self, x, y):
"""Return True iff the point falls sufficiently close to my core line
segment to count as a hit.
"""
if not super(Arrow, self).collide_point(x, y):
return False
orig = self.board.spotdict[unicode(self.portal.origin)]
dest = self.board.spotdict[unicode(self.portal.destination)]
(ox, oy) = orig.pos
(dx, dy) = dest.pos
if ox == dx:
return abs(y - dy) <= self.w
elif oy == dy:
return abs(x - dx) <= self.w
else:
correct_angle_a = atan2(dy, dx)
observed_angle_a = atan2(y, x)
error_angle_a = abs(observed_angle_a - correct_angle_a)
error_seg_len = hypot(x, y)
return sin(error_angle_a) * error_seg_len <= self.margin
def repawn(self, *args):
(branch, tick) = self.board.host.closet.time
for pawn in self.pawns_here:
locations = pawn.thing.get_locations(
self.board.facade.observer, branch)
bone = locations.value_during(tick)
t1 = bone.tick
try:
t2 = locations.key_after(tick)
except ValueError:
continue
if t2 is None:
continue
else:
duration = float(t2 - t1)
passed = float(tick - t1)
progress = passed / duration
os = self.board.spotdict[unicode(self.portal.origin)]
ds = self.board.spotdict[unicode(self.portal.destination)]
(ox, oy) = os.pos
(dx, dy) = ds.pos
w = dx - ox
h = dy - oy
x = ox + w * progress
y = oy + h * progress
pawn.pos = (x, y)
def on_pawns_here(self, i, v):
(branch, tick) = self.board.host.sanetime(None, None)
self.repawn(branch, tick)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment