You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
885 lines
26 KiB
885 lines
26 KiB
"""Geometrical Planes.
|
|
|
|
Contains
|
|
========
|
|
Plane
|
|
|
|
"""
|
|
|
|
from sympy.core import Dummy, Rational, S, Symbol
|
|
from sympy.core.symbol import _symbol
|
|
from sympy.functions.elementary.trigonometric import cos, sin, acos, asin, sqrt
|
|
from .entity import GeometryEntity
|
|
from .line import (Line, Ray, Segment, Line3D, LinearEntity, LinearEntity3D,
|
|
Ray3D, Segment3D)
|
|
from .point import Point, Point3D
|
|
from sympy.matrices import Matrix
|
|
from sympy.polys.polytools import cancel
|
|
from sympy.solvers import solve, linsolve
|
|
from sympy.utilities.iterables import uniq, is_sequence
|
|
from sympy.utilities.misc import filldedent, func_name, Undecidable
|
|
|
|
from mpmath.libmp.libmpf import prec_to_dps
|
|
|
|
import random
|
|
|
|
|
|
x, y, z, t = [Dummy('plane_dummy') for i in range(4)]
|
|
|
|
|
|
class Plane(GeometryEntity):
|
|
"""
|
|
A plane is a flat, two-dimensional surface. A plane is the two-dimensional
|
|
analogue of a point (zero-dimensions), a line (one-dimension) and a solid
|
|
(three-dimensions). A plane can generally be constructed by two types of
|
|
inputs. They are three non-collinear points and a point and the plane's
|
|
normal vector.
|
|
|
|
Attributes
|
|
==========
|
|
|
|
p1
|
|
normal_vector
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane, Point3D
|
|
>>> Plane(Point3D(1, 1, 1), Point3D(2, 3, 4), Point3D(2, 2, 2))
|
|
Plane(Point3D(1, 1, 1), (-1, 2, -1))
|
|
>>> Plane((1, 1, 1), (2, 3, 4), (2, 2, 2))
|
|
Plane(Point3D(1, 1, 1), (-1, 2, -1))
|
|
>>> Plane(Point3D(1, 1, 1), normal_vector=(1,4,7))
|
|
Plane(Point3D(1, 1, 1), (1, 4, 7))
|
|
|
|
"""
|
|
def __new__(cls, p1, a=None, b=None, **kwargs):
|
|
p1 = Point3D(p1, dim=3)
|
|
if a and b:
|
|
p2 = Point(a, dim=3)
|
|
p3 = Point(b, dim=3)
|
|
if Point3D.are_collinear(p1, p2, p3):
|
|
raise ValueError('Enter three non-collinear points')
|
|
a = p1.direction_ratio(p2)
|
|
b = p1.direction_ratio(p3)
|
|
normal_vector = tuple(Matrix(a).cross(Matrix(b)))
|
|
else:
|
|
a = kwargs.pop('normal_vector', a)
|
|
evaluate = kwargs.get('evaluate', True)
|
|
if is_sequence(a) and len(a) == 3:
|
|
normal_vector = Point3D(a).args if evaluate else a
|
|
else:
|
|
raise ValueError(filldedent('''
|
|
Either provide 3 3D points or a point with a
|
|
normal vector expressed as a sequence of length 3'''))
|
|
if all(coord.is_zero for coord in normal_vector):
|
|
raise ValueError('Normal vector cannot be zero vector')
|
|
return GeometryEntity.__new__(cls, p1, normal_vector, **kwargs)
|
|
|
|
def __contains__(self, o):
|
|
k = self.equation(x, y, z)
|
|
if isinstance(o, (LinearEntity, LinearEntity3D)):
|
|
d = Point3D(o.arbitrary_point(t))
|
|
e = k.subs([(x, d.x), (y, d.y), (z, d.z)])
|
|
return e.equals(0)
|
|
try:
|
|
o = Point(o, dim=3, strict=True)
|
|
d = k.xreplace(dict(zip((x, y, z), o.args)))
|
|
return d.equals(0)
|
|
except TypeError:
|
|
return False
|
|
|
|
def _eval_evalf(self, prec=15, **options):
|
|
pt, tup = self.args
|
|
dps = prec_to_dps(prec)
|
|
pt = pt.evalf(n=dps, **options)
|
|
tup = tuple([i.evalf(n=dps, **options) for i in tup])
|
|
return self.func(pt, normal_vector=tup, evaluate=False)
|
|
|
|
def angle_between(self, o):
|
|
"""Angle between the plane and other geometric entity.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
LinearEntity3D, Plane.
|
|
|
|
Returns
|
|
=======
|
|
|
|
angle : angle in radians
|
|
|
|
Notes
|
|
=====
|
|
|
|
This method accepts only 3D entities as it's parameter, but if you want
|
|
to calculate the angle between a 2D entity and a plane you should
|
|
first convert to a 3D entity by projecting onto a desired plane and
|
|
then proceed to calculate the angle.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Line3D, Plane
|
|
>>> a = Plane(Point3D(1, 2, 2), normal_vector=(1, 2, 3))
|
|
>>> b = Line3D(Point3D(1, 3, 4), Point3D(2, 2, 2))
|
|
>>> a.angle_between(b)
|
|
-asin(sqrt(21)/6)
|
|
|
|
"""
|
|
if isinstance(o, LinearEntity3D):
|
|
a = Matrix(self.normal_vector)
|
|
b = Matrix(o.direction_ratio)
|
|
c = a.dot(b)
|
|
d = sqrt(sum([i**2 for i in self.normal_vector]))
|
|
e = sqrt(sum([i**2 for i in o.direction_ratio]))
|
|
return asin(c/(d*e))
|
|
if isinstance(o, Plane):
|
|
a = Matrix(self.normal_vector)
|
|
b = Matrix(o.normal_vector)
|
|
c = a.dot(b)
|
|
d = sqrt(sum([i**2 for i in self.normal_vector]))
|
|
e = sqrt(sum([i**2 for i in o.normal_vector]))
|
|
return acos(c/(d*e))
|
|
|
|
|
|
def arbitrary_point(self, u=None, v=None):
|
|
""" Returns an arbitrary point on the Plane. If given two
|
|
parameters, the point ranges over the entire plane. If given 1
|
|
or no parameters, returns a point with one parameter which,
|
|
when varying from 0 to 2*pi, moves the point in a circle of
|
|
radius 1 about p1 of the Plane.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane, Ray
|
|
>>> from sympy.abc import u, v, t, r
|
|
>>> p = Plane((1, 1, 1), normal_vector=(1, 0, 0))
|
|
>>> p.arbitrary_point(u, v)
|
|
Point3D(1, u + 1, v + 1)
|
|
>>> p.arbitrary_point(t)
|
|
Point3D(1, cos(t) + 1, sin(t) + 1)
|
|
|
|
While arbitrary values of u and v can move the point anywhere in
|
|
the plane, the single-parameter point can be used to construct a
|
|
ray whose arbitrary point can be located at angle t and radius
|
|
r from p.p1:
|
|
|
|
>>> Ray(p.p1, _).arbitrary_point(r)
|
|
Point3D(1, r*cos(t) + 1, r*sin(t) + 1)
|
|
|
|
Returns
|
|
=======
|
|
|
|
Point3D
|
|
|
|
"""
|
|
circle = v is None
|
|
if circle:
|
|
u = _symbol(u or 't', real=True)
|
|
else:
|
|
u = _symbol(u or 'u', real=True)
|
|
v = _symbol(v or 'v', real=True)
|
|
x, y, z = self.normal_vector
|
|
a, b, c = self.p1.args
|
|
# x1, y1, z1 is a nonzero vector parallel to the plane
|
|
if x.is_zero and y.is_zero:
|
|
x1, y1, z1 = S.One, S.Zero, S.Zero
|
|
else:
|
|
x1, y1, z1 = -y, x, S.Zero
|
|
# x2, y2, z2 is also parallel to the plane, and orthogonal to x1, y1, z1
|
|
x2, y2, z2 = tuple(Matrix((x, y, z)).cross(Matrix((x1, y1, z1))))
|
|
if circle:
|
|
x1, y1, z1 = (w/sqrt(x1**2 + y1**2 + z1**2) for w in (x1, y1, z1))
|
|
x2, y2, z2 = (w/sqrt(x2**2 + y2**2 + z2**2) for w in (x2, y2, z2))
|
|
p = Point3D(a + x1*cos(u) + x2*sin(u), \
|
|
b + y1*cos(u) + y2*sin(u), \
|
|
c + z1*cos(u) + z2*sin(u))
|
|
else:
|
|
p = Point3D(a + x1*u + x2*v, b + y1*u + y2*v, c + z1*u + z2*v)
|
|
return p
|
|
|
|
|
|
@staticmethod
|
|
def are_concurrent(*planes):
|
|
"""Is a sequence of Planes concurrent?
|
|
|
|
Two or more Planes are concurrent if their intersections
|
|
are a common line.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
planes: list
|
|
|
|
Returns
|
|
=======
|
|
|
|
Boolean
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane, Point3D
|
|
>>> a = Plane(Point3D(5, 0, 0), normal_vector=(1, -1, 1))
|
|
>>> b = Plane(Point3D(0, -2, 0), normal_vector=(3, 1, 1))
|
|
>>> c = Plane(Point3D(0, -1, 0), normal_vector=(5, -1, 9))
|
|
>>> Plane.are_concurrent(a, b)
|
|
True
|
|
>>> Plane.are_concurrent(a, b, c)
|
|
False
|
|
|
|
"""
|
|
planes = list(uniq(planes))
|
|
for i in planes:
|
|
if not isinstance(i, Plane):
|
|
raise ValueError('All objects should be Planes but got %s' % i.func)
|
|
if len(planes) < 2:
|
|
return False
|
|
planes = list(planes)
|
|
first = planes.pop(0)
|
|
sol = first.intersection(planes[0])
|
|
if sol == []:
|
|
return False
|
|
else:
|
|
line = sol[0]
|
|
for i in planes[1:]:
|
|
l = first.intersection(i)
|
|
if not l or l[0] not in line:
|
|
return False
|
|
return True
|
|
|
|
|
|
def distance(self, o):
|
|
"""Distance between the plane and another geometric entity.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
Point3D, LinearEntity3D, Plane.
|
|
|
|
Returns
|
|
=======
|
|
|
|
distance
|
|
|
|
Notes
|
|
=====
|
|
|
|
This method accepts only 3D entities as it's parameter, but if you want
|
|
to calculate the distance between a 2D entity and a plane you should
|
|
first convert to a 3D entity by projecting onto a desired plane and
|
|
then proceed to calculate the distance.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Line3D, Plane
|
|
>>> a = Plane(Point3D(1, 1, 1), normal_vector=(1, 1, 1))
|
|
>>> b = Point3D(1, 2, 3)
|
|
>>> a.distance(b)
|
|
sqrt(3)
|
|
>>> c = Line3D(Point3D(2, 3, 1), Point3D(1, 2, 2))
|
|
>>> a.distance(c)
|
|
0
|
|
|
|
"""
|
|
if self.intersection(o) != []:
|
|
return S.Zero
|
|
|
|
if isinstance(o, (Segment3D, Ray3D)):
|
|
a, b = o.p1, o.p2
|
|
pi, = self.intersection(Line3D(a, b))
|
|
if pi in o:
|
|
return self.distance(pi)
|
|
elif a in Segment3D(pi, b):
|
|
return self.distance(a)
|
|
else:
|
|
assert isinstance(o, Segment3D) is True
|
|
return self.distance(b)
|
|
|
|
# following code handles `Point3D`, `LinearEntity3D`, `Plane`
|
|
a = o if isinstance(o, Point3D) else o.p1
|
|
n = Point3D(self.normal_vector).unit
|
|
d = (a - self.p1).dot(n)
|
|
return abs(d)
|
|
|
|
|
|
def equals(self, o):
|
|
"""
|
|
Returns True if self and o are the same mathematical entities.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane, Point3D
|
|
>>> a = Plane(Point3D(1, 2, 3), normal_vector=(1, 1, 1))
|
|
>>> b = Plane(Point3D(1, 2, 3), normal_vector=(2, 2, 2))
|
|
>>> c = Plane(Point3D(1, 2, 3), normal_vector=(-1, 4, 6))
|
|
>>> a.equals(a)
|
|
True
|
|
>>> a.equals(b)
|
|
True
|
|
>>> a.equals(c)
|
|
False
|
|
"""
|
|
if isinstance(o, Plane):
|
|
a = self.equation()
|
|
b = o.equation()
|
|
return cancel(a/b).is_constant()
|
|
else:
|
|
return False
|
|
|
|
|
|
def equation(self, x=None, y=None, z=None):
|
|
"""The equation of the Plane.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Plane
|
|
>>> a = Plane(Point3D(1, 1, 2), Point3D(2, 4, 7), Point3D(3, 5, 1))
|
|
>>> a.equation()
|
|
-23*x + 11*y - 2*z + 16
|
|
>>> a = Plane(Point3D(1, 4, 2), normal_vector=(6, 6, 6))
|
|
>>> a.equation()
|
|
6*x + 6*y + 6*z - 42
|
|
|
|
"""
|
|
x, y, z = [i if i else Symbol(j, real=True) for i, j in zip((x, y, z), 'xyz')]
|
|
a = Point3D(x, y, z)
|
|
b = self.p1.direction_ratio(a)
|
|
c = self.normal_vector
|
|
return (sum(i*j for i, j in zip(b, c)))
|
|
|
|
|
|
def intersection(self, o):
|
|
""" The intersection with other geometrical entity.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
Point, Point3D, LinearEntity, LinearEntity3D, Plane
|
|
|
|
Returns
|
|
=======
|
|
|
|
List
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Line3D, Plane
|
|
>>> a = Plane(Point3D(1, 2, 3), normal_vector=(1, 1, 1))
|
|
>>> b = Point3D(1, 2, 3)
|
|
>>> a.intersection(b)
|
|
[Point3D(1, 2, 3)]
|
|
>>> c = Line3D(Point3D(1, 4, 7), Point3D(2, 2, 2))
|
|
>>> a.intersection(c)
|
|
[Point3D(2, 2, 2)]
|
|
>>> d = Plane(Point3D(6, 0, 0), normal_vector=(2, -5, 3))
|
|
>>> e = Plane(Point3D(2, 0, 0), normal_vector=(3, 4, -3))
|
|
>>> d.intersection(e)
|
|
[Line3D(Point3D(78/23, -24/23, 0), Point3D(147/23, 321/23, 23))]
|
|
|
|
"""
|
|
if not isinstance(o, GeometryEntity):
|
|
o = Point(o, dim=3)
|
|
if isinstance(o, Point):
|
|
if o in self:
|
|
return [o]
|
|
else:
|
|
return []
|
|
if isinstance(o, (LinearEntity, LinearEntity3D)):
|
|
# recast to 3D
|
|
p1, p2 = o.p1, o.p2
|
|
if isinstance(o, Segment):
|
|
o = Segment3D(p1, p2)
|
|
elif isinstance(o, Ray):
|
|
o = Ray3D(p1, p2)
|
|
elif isinstance(o, Line):
|
|
o = Line3D(p1, p2)
|
|
else:
|
|
raise ValueError('unhandled linear entity: %s' % o.func)
|
|
if o in self:
|
|
return [o]
|
|
else:
|
|
a = Point3D(o.arbitrary_point(t))
|
|
p1, n = self.p1, Point3D(self.normal_vector)
|
|
|
|
# TODO: Replace solve with solveset, when this line is tested
|
|
c = solve((a - p1).dot(n), t)
|
|
if not c:
|
|
return []
|
|
else:
|
|
c = [i for i in c if i.is_real is not False]
|
|
if len(c) > 1:
|
|
c = [i for i in c if i.is_real]
|
|
if len(c) != 1:
|
|
raise Undecidable("not sure which point is real")
|
|
p = a.subs(t, c[0])
|
|
if p not in o:
|
|
return [] # e.g. a segment might not intersect a plane
|
|
return [p]
|
|
if isinstance(o, Plane):
|
|
if self.equals(o):
|
|
return [self]
|
|
if self.is_parallel(o):
|
|
return []
|
|
else:
|
|
x, y, z = map(Dummy, 'xyz')
|
|
a, b = Matrix([self.normal_vector]), Matrix([o.normal_vector])
|
|
c = list(a.cross(b))
|
|
d = self.equation(x, y, z)
|
|
e = o.equation(x, y, z)
|
|
result = list(linsolve([d, e], x, y, z))[0]
|
|
for i in (x, y, z): result = result.subs(i, 0)
|
|
return [Line3D(Point3D(result), direction_ratio=c)]
|
|
|
|
|
|
def is_coplanar(self, o):
|
|
""" Returns True if `o` is coplanar with self, else False.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane
|
|
>>> o = (0, 0, 0)
|
|
>>> p = Plane(o, (1, 1, 1))
|
|
>>> p2 = Plane(o, (2, 2, 2))
|
|
>>> p == p2
|
|
False
|
|
>>> p.is_coplanar(p2)
|
|
True
|
|
"""
|
|
if isinstance(o, Plane):
|
|
return not cancel(self.equation(x, y, z)/o.equation(x, y, z)).has(x, y, z)
|
|
if isinstance(o, Point3D):
|
|
return o in self
|
|
elif isinstance(o, LinearEntity3D):
|
|
return all(i in self for i in self)
|
|
elif isinstance(o, GeometryEntity): # XXX should only be handling 2D objects now
|
|
return all(i == 0 for i in self.normal_vector[:2])
|
|
|
|
|
|
def is_parallel(self, l):
|
|
"""Is the given geometric entity parallel to the plane?
|
|
|
|
Parameters
|
|
==========
|
|
|
|
LinearEntity3D or Plane
|
|
|
|
Returns
|
|
=======
|
|
|
|
Boolean
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane, Point3D
|
|
>>> a = Plane(Point3D(1,4,6), normal_vector=(2, 4, 6))
|
|
>>> b = Plane(Point3D(3,1,3), normal_vector=(4, 8, 12))
|
|
>>> a.is_parallel(b)
|
|
True
|
|
|
|
"""
|
|
if isinstance(l, LinearEntity3D):
|
|
a = l.direction_ratio
|
|
b = self.normal_vector
|
|
c = sum([i*j for i, j in zip(a, b)])
|
|
if c == 0:
|
|
return True
|
|
else:
|
|
return False
|
|
elif isinstance(l, Plane):
|
|
a = Matrix(l.normal_vector)
|
|
b = Matrix(self.normal_vector)
|
|
if a.cross(b).is_zero_matrix:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def is_perpendicular(self, l):
|
|
"""Is the given geometric entity perpendicualar to the given plane?
|
|
|
|
Parameters
|
|
==========
|
|
|
|
LinearEntity3D or Plane
|
|
|
|
Returns
|
|
=======
|
|
|
|
Boolean
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane, Point3D
|
|
>>> a = Plane(Point3D(1,4,6), normal_vector=(2, 4, 6))
|
|
>>> b = Plane(Point3D(2, 2, 2), normal_vector=(-1, 2, -1))
|
|
>>> a.is_perpendicular(b)
|
|
True
|
|
|
|
"""
|
|
if isinstance(l, LinearEntity3D):
|
|
a = Matrix(l.direction_ratio)
|
|
b = Matrix(self.normal_vector)
|
|
if a.cross(b).is_zero_matrix:
|
|
return True
|
|
else:
|
|
return False
|
|
elif isinstance(l, Plane):
|
|
a = Matrix(l.normal_vector)
|
|
b = Matrix(self.normal_vector)
|
|
if a.dot(b) == 0:
|
|
return True
|
|
else:
|
|
return False
|
|
else:
|
|
return False
|
|
|
|
@property
|
|
def normal_vector(self):
|
|
"""Normal vector of the given plane.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Plane
|
|
>>> a = Plane(Point3D(1, 1, 1), Point3D(2, 3, 4), Point3D(2, 2, 2))
|
|
>>> a.normal_vector
|
|
(-1, 2, -1)
|
|
>>> a = Plane(Point3D(1, 1, 1), normal_vector=(1, 4, 7))
|
|
>>> a.normal_vector
|
|
(1, 4, 7)
|
|
|
|
"""
|
|
return self.args[1]
|
|
|
|
@property
|
|
def p1(self):
|
|
"""The only defining point of the plane. Others can be obtained from the
|
|
arbitrary_point method.
|
|
|
|
See Also
|
|
========
|
|
|
|
sympy.geometry.point.Point3D
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Point3D, Plane
|
|
>>> a = Plane(Point3D(1, 1, 1), Point3D(2, 3, 4), Point3D(2, 2, 2))
|
|
>>> a.p1
|
|
Point3D(1, 1, 1)
|
|
|
|
"""
|
|
return self.args[0]
|
|
|
|
def parallel_plane(self, pt):
|
|
"""
|
|
Plane parallel to the given plane and passing through the point pt.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
pt: Point3D
|
|
|
|
Returns
|
|
=======
|
|
|
|
Plane
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane, Point3D
|
|
>>> a = Plane(Point3D(1, 4, 6), normal_vector=(2, 4, 6))
|
|
>>> a.parallel_plane(Point3D(2, 3, 5))
|
|
Plane(Point3D(2, 3, 5), (2, 4, 6))
|
|
|
|
"""
|
|
a = self.normal_vector
|
|
return Plane(pt, normal_vector=a)
|
|
|
|
def perpendicular_line(self, pt):
|
|
"""A line perpendicular to the given plane.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
pt: Point3D
|
|
|
|
Returns
|
|
=======
|
|
|
|
Line3D
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane, Point3D
|
|
>>> a = Plane(Point3D(1,4,6), normal_vector=(2, 4, 6))
|
|
>>> a.perpendicular_line(Point3D(9, 8, 7))
|
|
Line3D(Point3D(9, 8, 7), Point3D(11, 12, 13))
|
|
|
|
"""
|
|
a = self.normal_vector
|
|
return Line3D(pt, direction_ratio=a)
|
|
|
|
def perpendicular_plane(self, *pts):
|
|
"""
|
|
Return a perpendicular passing through the given points. If the
|
|
direction ratio between the points is the same as the Plane's normal
|
|
vector then, to select from the infinite number of possible planes,
|
|
a third point will be chosen on the z-axis (or the y-axis
|
|
if the normal vector is already parallel to the z-axis). If less than
|
|
two points are given they will be supplied as follows: if no point is
|
|
given then pt1 will be self.p1; if a second point is not given it will
|
|
be a point through pt1 on a line parallel to the z-axis (if the normal
|
|
is not already the z-axis, otherwise on the line parallel to the
|
|
y-axis).
|
|
|
|
Parameters
|
|
==========
|
|
|
|
pts: 0, 1 or 2 Point3D
|
|
|
|
Returns
|
|
=======
|
|
|
|
Plane
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane, Point3D
|
|
>>> a, b = Point3D(0, 0, 0), Point3D(0, 1, 0)
|
|
>>> Z = (0, 0, 1)
|
|
>>> p = Plane(a, normal_vector=Z)
|
|
>>> p.perpendicular_plane(a, b)
|
|
Plane(Point3D(0, 0, 0), (1, 0, 0))
|
|
"""
|
|
if len(pts) > 2:
|
|
raise ValueError('No more than 2 pts should be provided.')
|
|
|
|
pts = list(pts)
|
|
if len(pts) == 0:
|
|
pts.append(self.p1)
|
|
if len(pts) == 1:
|
|
x, y, z = self.normal_vector
|
|
if x == y == 0:
|
|
dir = (0, 1, 0)
|
|
else:
|
|
dir = (0, 0, 1)
|
|
pts.append(pts[0] + Point3D(*dir))
|
|
|
|
p1, p2 = [Point(i, dim=3) for i in pts]
|
|
l = Line3D(p1, p2)
|
|
n = Line3D(p1, direction_ratio=self.normal_vector)
|
|
if l in n: # XXX should an error be raised instead?
|
|
# there are infinitely many perpendicular planes;
|
|
x, y, z = self.normal_vector
|
|
if x == y == 0:
|
|
# the z axis is the normal so pick a pt on the y-axis
|
|
p3 = Point3D(0, 1, 0) # case 1
|
|
else:
|
|
# else pick a pt on the z axis
|
|
p3 = Point3D(0, 0, 1) # case 2
|
|
# in case that point is already given, move it a bit
|
|
if p3 in l:
|
|
p3 *= 2 # case 3
|
|
else:
|
|
p3 = p1 + Point3D(*self.normal_vector) # case 4
|
|
return Plane(p1, p2, p3)
|
|
|
|
def projection_line(self, line):
|
|
"""Project the given line onto the plane through the normal plane
|
|
containing the line.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
LinearEntity or LinearEntity3D
|
|
|
|
Returns
|
|
=======
|
|
|
|
Point3D, Line3D, Ray3D or Segment3D
|
|
|
|
Notes
|
|
=====
|
|
|
|
For the interaction between 2D and 3D lines(segments, rays), you should
|
|
convert the line to 3D by using this method. For example for finding the
|
|
intersection between a 2D and a 3D line, convert the 2D line to a 3D line
|
|
by projecting it on a required plane and then proceed to find the
|
|
intersection between those lines.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane, Line, Line3D, Point3D
|
|
>>> a = Plane(Point3D(1, 1, 1), normal_vector=(1, 1, 1))
|
|
>>> b = Line(Point3D(1, 1), Point3D(2, 2))
|
|
>>> a.projection_line(b)
|
|
Line3D(Point3D(4/3, 4/3, 1/3), Point3D(5/3, 5/3, -1/3))
|
|
>>> c = Line3D(Point3D(1, 1, 1), Point3D(2, 2, 2))
|
|
>>> a.projection_line(c)
|
|
Point3D(1, 1, 1)
|
|
|
|
"""
|
|
if not isinstance(line, (LinearEntity, LinearEntity3D)):
|
|
raise NotImplementedError('Enter a linear entity only')
|
|
a, b = self.projection(line.p1), self.projection(line.p2)
|
|
if a == b:
|
|
# projection does not imply intersection so for
|
|
# this case (line parallel to plane's normal) we
|
|
# return the projection point
|
|
return a
|
|
if isinstance(line, (Line, Line3D)):
|
|
return Line3D(a, b)
|
|
if isinstance(line, (Ray, Ray3D)):
|
|
return Ray3D(a, b)
|
|
if isinstance(line, (Segment, Segment3D)):
|
|
return Segment3D(a, b)
|
|
|
|
def projection(self, pt):
|
|
"""Project the given point onto the plane along the plane normal.
|
|
|
|
Parameters
|
|
==========
|
|
|
|
Point or Point3D
|
|
|
|
Returns
|
|
=======
|
|
|
|
Point3D
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane, Point3D
|
|
>>> A = Plane(Point3D(1, 1, 2), normal_vector=(1, 1, 1))
|
|
|
|
The projection is along the normal vector direction, not the z
|
|
axis, so (1, 1) does not project to (1, 1, 2) on the plane A:
|
|
|
|
>>> b = Point3D(1, 1)
|
|
>>> A.projection(b)
|
|
Point3D(5/3, 5/3, 2/3)
|
|
>>> _ in A
|
|
True
|
|
|
|
But the point (1, 1, 2) projects to (1, 1) on the XY-plane:
|
|
|
|
>>> XY = Plane((0, 0, 0), (0, 0, 1))
|
|
>>> XY.projection((1, 1, 2))
|
|
Point3D(1, 1, 0)
|
|
"""
|
|
rv = Point(pt, dim=3)
|
|
if rv in self:
|
|
return rv
|
|
return self.intersection(Line3D(rv, rv + Point3D(self.normal_vector)))[0]
|
|
|
|
def random_point(self, seed=None):
|
|
""" Returns a random point on the Plane.
|
|
|
|
Returns
|
|
=======
|
|
|
|
Point3D
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import Plane
|
|
>>> p = Plane((1, 0, 0), normal_vector=(0, 1, 0))
|
|
>>> r = p.random_point(seed=42) # seed value is optional
|
|
>>> r.n(3)
|
|
Point3D(2.29, 0, -1.35)
|
|
|
|
The random point can be moved to lie on the circle of radius
|
|
1 centered on p1:
|
|
|
|
>>> c = p.p1 + (r - p.p1).unit
|
|
>>> c.distance(p.p1).equals(1)
|
|
True
|
|
"""
|
|
if seed is not None:
|
|
rng = random.Random(seed)
|
|
else:
|
|
rng = random
|
|
params = {
|
|
x: 2*Rational(rng.gauss(0, 1)) - 1,
|
|
y: 2*Rational(rng.gauss(0, 1)) - 1}
|
|
return self.arbitrary_point(x, y).subs(params)
|
|
|
|
def parameter_value(self, other, u, v=None):
|
|
"""Return the parameter(s) corresponding to the given point.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import pi, Plane
|
|
>>> from sympy.abc import t, u, v
|
|
>>> p = Plane((2, 0, 0), (0, 0, 1), (0, 1, 0))
|
|
|
|
By default, the parameter value returned defines a point
|
|
that is a distance of 1 from the Plane's p1 value and
|
|
in line with the given point:
|
|
|
|
>>> on_circle = p.arbitrary_point(t).subs(t, pi/4)
|
|
>>> on_circle.distance(p.p1)
|
|
1
|
|
>>> p.parameter_value(on_circle, t)
|
|
{t: pi/4}
|
|
|
|
Moving the point twice as far from p1 does not change
|
|
the parameter value:
|
|
|
|
>>> off_circle = p.p1 + (on_circle - p.p1)*2
|
|
>>> off_circle.distance(p.p1)
|
|
2
|
|
>>> p.parameter_value(off_circle, t)
|
|
{t: pi/4}
|
|
|
|
If the 2-value parameter is desired, supply the two
|
|
parameter symbols and a replacement dictionary will
|
|
be returned:
|
|
|
|
>>> p.parameter_value(on_circle, u, v)
|
|
{u: sqrt(10)/10, v: sqrt(10)/30}
|
|
>>> p.parameter_value(off_circle, u, v)
|
|
{u: sqrt(10)/5, v: sqrt(10)/15}
|
|
"""
|
|
if not isinstance(other, GeometryEntity):
|
|
other = Point(other, dim=self.ambient_dimension)
|
|
if not isinstance(other, Point):
|
|
raise ValueError("other must be a point")
|
|
if other == self.p1:
|
|
return other
|
|
if isinstance(u, Symbol) and v is None:
|
|
delta = self.arbitrary_point(u) - self.p1
|
|
eq = delta - (other - self.p1).unit
|
|
sol = solve(eq, u, dict=True)
|
|
elif isinstance(u, Symbol) and isinstance(v, Symbol):
|
|
pt = self.arbitrary_point(u, v)
|
|
sol = solve(pt - other, (u, v), dict=True)
|
|
else:
|
|
raise ValueError('expecting 1 or 2 symbols')
|
|
if not sol:
|
|
raise ValueError("Given point is not on %s" % func_name(self))
|
|
return sol[0] # {t: tval} or {u: uval, v: vval}
|
|
|
|
@property
|
|
def ambient_dimension(self):
|
|
return self.p1.ambient_dimension
|