Commit ef0841ed authored by Michael Büsch's avatar Michael Büsch

gui: Add 2D geometry classes

Signed-off-by: Michael Büsch's avatarMichael Buesch <m@bues.ch>
parent 939ffcc0
# -*- coding: utf-8 -*-
#
# AWL simulator - 2D geometry
#
# Copyright 2017 Michael Buesch <m@bues.ch>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
from __future__ import division, absolute_import, print_function, unicode_literals
from awlsim.common.compat import *
class Base2D(object):
EPSILON = 0.000001
def __hash__(self):
return id(self)
def __eq__(self, other):
return self is other
def __ne__(self, other):
return not self.__eq__(other)
class Point2D(Base2D):
"""2D point.
"""
__slots__ = ( "x", "y", )
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __eq__(self, other):
return other is not None and\
self.x == other.x and self.y == other.y
def __repr__(self):
return "Point2D(x=%f, y=%f)" % (self.x, self.y)
class Vect2D(Base2D):
"""2D vector.
"""
__slots__ = ( "x", "y", )
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __eq__(self, other):
return other is not None and\
self.x == other.x and self.y == other.y
def __repr__(self):
return "Vect2D(x=%f, y=%f)" % (self.x, self.y)
class Inter2D(Base2D):
"""Intersection information for two 2D line segments.
"""
__slots__ = ( "point", "vect", "__intersects", )
def __init__(self, point=None, vect=None, intersects=False):
"""point => Intersection point, or None.
vect => Intersection vector.
None or Vect2D(0, 0), if the intersection is only in one point.
intersects => True, if there is an intersection.
"""
self.point = point
self.vect = vect or Vect2D()
self.__intersects = intersects
def __eq__(self, other):
return other is not None and\
self.point == other.point and\
self.vect == other.vect and\
self.__intersects == other.__intersects
@property
def intersects(self):
"""Returns True, if there is an intersection.
"""
return self.__intersects and\
self.point is not None
@property
def lineSeg(self):
"""Get the line segment of the intersection.
Returns None, if self.point is None.
"""
if self.point:
return LineSeg2D(self.point,
Point2D(self.point.x + self.vect.x,
self.point.y + self.vect.y))
return None
def __repr__(self):
return "Inter2D(point=%s, vect=%s, intersects=%s)" % (
self.point, self.vect, self.__intersects)
class LineSeg2D(Base2D):
"""Line segment in 2D space.
"""
__slots__ = ( "pointA", "pointB", )
def __init__(self, pointA, pointB):
self.pointA = pointA
self.pointB = pointB
def __eq__(self, other):
return other is not None and\
self.pointA == other.pointA and\
self.pointB == other.pointB
@property
def isHorizontal(self):
"""Returns True, if the line segment is parallel to the X axis.
"""
return self.pointA.y == self.pointB.y
@property
def isVertical(self):
"""Returns True, if the line segment is parallel to the Y axis.
"""
return self.pointA.x == self.pointB.x
@property
def slope(self):
"""Get the slope of the line segment.
Raises ZeroDivisionError if the line segment is vertical.
"""
return float(self.pointB.y - self.pointA.y) / \
(self.pointB.x - self.pointA.x)
@property
def intercept(self):
"""Get the Y value of the Y axis crossing of this line.
Raises ZeroDivisionError if the line segment is vertical.
"""
return self.pointA.y - (self.pointA.x * self.slope)
@property
def vect(self):
"""Get the line segment vector.
"""
return Vect2D(-self.pointA.x + self.pointB.x,
-self.pointA.y + self.pointB.y)
@staticmethod
def __inRect(x, y, diaPointA, diaPointB):
"""Check if point (x,y) is within an axis-aligned rect
with the diagonal (diaPointA,diaPointB).
"""
diaMinX, diaMaxX = min(diaPointA.x, diaPointB.x),\
max(diaPointA.x, diaPointB.x)
diaMinY, diaMaxY = min(diaPointA.y, diaPointB.y),\
max(diaPointA.y, diaPointB.y)
return (x >= diaMinX and x <= diaMaxX and\
y >= diaMinY and y <= diaMaxY)
def __intersectionAligned(self, other):
"""Get the intersection (if any) of two aligned line segments.
'self' and 'other' must be aligned in order for this to
return correct results.
"""
for interPoint in (other.pointA, other.pointB):
x, y = interPoint.x, interPoint.y
if self.__inRect(x, y, self.pointA, self.pointB):
if self.__inRect(self.pointA.x, self.pointA.y,
other.pointA, other.pointB):
vect = Vect2D(self.pointA.x - x,
self.pointA.y - y)
else:
vect = Vect2D(self.pointB.x - x,
self.pointB.y - y)
return Inter2D(point=Point2D(x, y),
vect=vect,
intersects=True)
return Inter2D()
def __intersectionVertical(self, other):
"""Get the intersection of a vertical line segment 'self'
and a non-vertical line segment 'other'.
'self' must be vertical in order for this to
return correct results.
"""
x = self.pointA.x
y = (x - other.pointA.x) * other.slope + other.pointA.y
return Inter2D(point=Point2D(x, y),
intersects=self.__inRect(
x, y, self.pointA, self.pointB))
def intersection(self, other):
"""Get the intersection of this line segment
with another line segment.
Returns an Inter2D.
"""
try:
selfVert, otherVert = self.isVertical, other.isVertical
if selfVert and otherVert:
# Both line segments are vertical.
# If they are aligned, they might overlap.
if self.pointA.x == other.pointA.x:
return self.__intersectionAligned(other)
return Inter2D()
elif selfVert:
# self is vertical. other is not vertical.
return self.__intersectionVertical(other)
elif otherVert:
# self is not vertical. other is vertical.
return other.__intersectionVertical(self)
# Get the intersection of two arbitrary
# non-vertical line segments.
selfSlope, otherSlope = self.slope, other.slope
selfInter, otherInter = self.intercept, other.intercept
try:
x = (otherInter - selfInter) / \
(selfSlope - otherSlope)
y = selfSlope * x + selfInter
assert(abs(y - (otherSlope * x + otherInter)) < self.EPSILON)
except ZeroDivisionError:
# The line segments are parallel.
# If they have the same intercept they are aligned
# and might overlap.
if abs(selfInter - otherInter) < self.EPSILON:
return self.__intersectionAligned(other)
return Inter2D()
return Inter2D(point=Point2D(x, y),
intersects=self.__inRect(
x, y, self.pointA, self.pointB))
except ZeroDivisionError:
pass
return Inter2D()
def __repr__(self):
return "LineSeg2D(pointA=%s, pointB=%s)" % (
self.pointA, self.pointB)
from __future__ import division, absolute_import, print_function, unicode_literals
from awlsim_tstlib import *
from awlsim.gui.geo2d import *
class Test_LineSeg2D(object):
def test_point(self):
p = Point2D()
assert_eq(p.x, 0)
assert_eq(p.y, 0)
p = Point2D(42, -42)
assert_eq(p.x, 42)
assert_eq(p.y, -42)
assert_eq(p, Point2D(42, -42))
assert_ne(p, Point2D(42, 42))
assert_ne(p, Point2D(-42, -42))
def test_vect(self):
v = Vect2D()
assert_eq(v.x, 0)
assert_eq(v.y, 0)
v = Vect2D(42, -42)
assert_eq(v.x, 42)
assert_eq(v.y, -42)
assert_eq(v, Vect2D(42, -42))
assert_ne(v, Vect2D(42, 42))
assert_ne(v, Vect2D(-42, -42))
def test_intersection_base(self):
inter = Inter2D()
assert_eq(inter, Inter2D())
assert_ne(inter, Inter2D(Point2D(1, 1)))
assert_is(inter.intersects, False)
assert_is(inter.point, None)
assert_eq(inter.vect, Vect2D())
inter = Inter2D(Point2D(42, -42), Vect2D(142, -142), False)
assert_eq(inter, Inter2D(Point2D(42, -42), Vect2D(142, -142), False))
assert_ne(inter, Inter2D(Point2D(42, -42), Vect2D(143, -142), False))
assert_is(inter.intersects, False)
assert_eq(inter.point, Point2D(42, -42))
assert_eq(inter.vect, Vect2D(142, -142))
inter = Inter2D(Point2D(42, -42), Vect2D(142, -142), True)
assert_eq(inter, Inter2D(Point2D(42, -42), Vect2D(142, -142), True))
assert_ne(inter, Inter2D(Point2D(42, -42), Vect2D(142, -142), False))
assert_is(inter.intersects, True)
assert_eq(inter.point, Point2D(42, -42))
assert_eq(inter.vect, Vect2D(142, -142))
inter = Inter2D(None, Vect2D(142, -142), True)
assert_is(inter.intersects, False)
assert_is(inter.point, None)
assert_eq(inter.vect, Vect2D(142, -142))
def test_intersection(self):
# intersecting
inter = LineSeg2D(Point2D(2, 7), Point2D(8, 1)).intersection(
LineSeg2D(Point2D(9, 8), Point2D(3, 2)))
assert_is(inter.intersects, True)
assert_eq(inter.point, Point2D(5, 4))
assert_eq(inter.vect, Vect2D())
assert_eq(inter.lineSeg, LineSeg2D(Point2D(5, 4), Point2D(5, 4)))
# not intersecting
inter = LineSeg2D(Point2D(1, 1), Point2D(4, 5)).intersection(
LineSeg2D(Point2D(6, 6), Point2D(8, 2)))
assert_is(inter.intersects, False)
assert_eq(inter.point, Point2D(5.5, 7))
assert_eq(inter.vect, Vect2D())
assert_eq(inter.lineSeg, LineSeg2D(Point2D(5.5, 7), Point2D(5.5, 7)))
# parallel, horizontal, not intersecting
inter = LineSeg2D(Point2D(3, 2), Point2D(9, 2)).intersection(
LineSeg2D(Point2D(5, 3), Point2D(14, 3)))
assert_is(inter.intersects, False)
assert_is_none(inter.point)
assert_eq(inter.vect, Vect2D())
assert_is_none(inter.lineSeg)
# parallel, vertical, not intersecting
inter = LineSeg2D(Point2D(3, 9), Point2D(3, 4)).intersection(
LineSeg2D(Point2D(5, 2), Point2D(5, 5)))
assert_is(inter.intersects, False)
assert_is_none(inter.point)
assert_eq(inter.vect, Vect2D())
assert_is_none(inter.lineSeg)
# parallel, horizontal, intersecting
inter = LineSeg2D(Point2D(3, 2), Point2D(9, 2)).intersection(
LineSeg2D(Point2D(5, 2), Point2D(14, 2)))
assert_is(inter.intersects, True)
assert_eq(inter.point, Point2D(5, 2))
assert_eq(inter.vect, Vect2D(4, 0))
assert_eq(inter.lineSeg, LineSeg2D(Point2D(5, 2), Point2D(9, 2)))
# parallel, horizontal, intersecting
inter = LineSeg2D(Point2D(3, 2), Point2D(9, 2)).intersection(
LineSeg2D(Point2D(14, 2), Point2D(5, 2)))
assert_is(inter.intersects, True)
assert_eq(inter.point, Point2D(5, 2))
assert_eq(inter.vect, Vect2D(4, 0))
assert_eq(inter.lineSeg, LineSeg2D(Point2D(5, 2), Point2D(9, 2)))
# parallel, horizontal, intersecting
inter = LineSeg2D(Point2D(9, 2), Point2D(3, 2)).intersection(
LineSeg2D(Point2D(14, 2), Point2D(5, 2)))
assert_is(inter.intersects, True)
assert_eq(inter.point, Point2D(5, 2))
assert_eq(inter.vect, Vect2D(4, 0))
assert_eq(inter.lineSeg, LineSeg2D(Point2D(5, 2), Point2D(9, 2)))
# parallel, horizontal, intersecting
inter = LineSeg2D(Point2D(5, 2), Point2D(14, 2)).intersection(
LineSeg2D(Point2D(3, 2), Point2D(9, 2)))
assert_is(inter.intersects, True)
assert_eq(inter.point, Point2D(9, 2))
assert_eq(inter.vect, Vect2D(-4, 0))
assert_eq(inter.lineSeg, LineSeg2D(Point2D(9, 2), Point2D(5, 2)))
# parallel, vertical, intersecting
inter = LineSeg2D(Point2D(3, 9), Point2D(3, 4)).intersection(
LineSeg2D(Point2D(3, 2), Point2D(3, 5)))
assert_is(inter.intersects, True)
assert_eq(inter.point, Point2D(3, 5))
assert_eq(inter.vect, Vect2D(0, -1))
assert_eq(inter.lineSeg, LineSeg2D(Point2D(3, 5), Point2D(3, 4)))
# parallel, vertical, intersecting
inter = LineSeg2D(Point2D(3, 2), Point2D(3, 5)).intersection(
LineSeg2D(Point2D(3, 9), Point2D(3, 4)))
assert_is(inter.intersects, True)
assert_eq(inter.point, Point2D(3, 4))
assert_eq(inter.vect, Vect2D(0, 1))
assert_eq(inter.lineSeg, LineSeg2D(Point2D(3, 4), Point2D(3, 5)))
# AB vertical, CD horizontal, intersecting
inter = LineSeg2D(Point2D(4, 1), Point2D(4, 8)).intersection(
LineSeg2D(Point2D(2, 5), Point2D(12, 5)))
assert_is(inter.intersects, True)
assert_eq(inter.point, Point2D(4, 5))
assert_eq(inter.vect, Vect2D())
assert_eq(inter.lineSeg, LineSeg2D(Point2D(4, 5), Point2D(4, 5)))
# AB horizontal, CD vertical, not intersecting
inter = LineSeg2D(Point2D(9, 5), Point2D(4, 5)).intersection(
LineSeg2D(Point2D(8, 6), Point2D(8, 11)))
assert_is(inter.intersects, False)
assert_eq(inter.point, Point2D(8, 5))
assert_eq(inter.vect, Vect2D())
assert_eq(inter.lineSeg, LineSeg2D(Point2D(8, 5), Point2D(8, 5)))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment