polygon.py 80 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891
  1. from sympy.core import Expr, S, oo, pi, sympify
  2. from sympy.core.evalf import N
  3. from sympy.core.sorting import default_sort_key, ordered
  4. from sympy.core.symbol import _symbol, Dummy, Symbol
  5. from sympy.functions.elementary.complexes import sign
  6. from sympy.functions.elementary.piecewise import Piecewise
  7. from sympy.functions.elementary.trigonometric import cos, sin, tan
  8. from .ellipse import Circle
  9. from .entity import GeometryEntity, GeometrySet
  10. from .exceptions import GeometryError
  11. from .line import Line, Segment, Ray
  12. from .point import Point
  13. from sympy.logic import And
  14. from sympy.matrices import Matrix
  15. from sympy.simplify.simplify import simplify
  16. from sympy.solvers.solvers import solve
  17. from sympy.utilities.iterables import has_dups, has_variety, uniq, rotate_left, least_rotation
  18. from sympy.utilities.misc import as_int, func_name
  19. from mpmath.libmp.libmpf import prec_to_dps
  20. import warnings
  21. x, y, T = [Dummy('polygon_dummy', real=True) for i in range(3)]
  22. class Polygon(GeometrySet):
  23. """A two-dimensional polygon.
  24. A simple polygon in space. Can be constructed from a sequence of points
  25. or from a center, radius, number of sides and rotation angle.
  26. Parameters
  27. ==========
  28. vertices
  29. A sequence of points.
  30. n : int, optional
  31. If $> 0$, an n-sided RegularPolygon is created.
  32. Default value is $0$.
  33. Attributes
  34. ==========
  35. area
  36. angles
  37. perimeter
  38. vertices
  39. centroid
  40. sides
  41. Raises
  42. ======
  43. GeometryError
  44. If all parameters are not Points.
  45. See Also
  46. ========
  47. sympy.geometry.point.Point, sympy.geometry.line.Segment, Triangle
  48. Notes
  49. =====
  50. Polygons are treated as closed paths rather than 2D areas so
  51. some calculations can be be negative or positive (e.g., area)
  52. based on the orientation of the points.
  53. Any consecutive identical points are reduced to a single point
  54. and any points collinear and between two points will be removed
  55. unless they are needed to define an explicit intersection (see examples).
  56. A Triangle, Segment or Point will be returned when there are 3 or
  57. fewer points provided.
  58. Examples
  59. ========
  60. >>> from sympy import Polygon, pi
  61. >>> p1, p2, p3, p4, p5 = [(0, 0), (1, 0), (5, 1), (0, 1), (3, 0)]
  62. >>> Polygon(p1, p2, p3, p4)
  63. Polygon(Point2D(0, 0), Point2D(1, 0), Point2D(5, 1), Point2D(0, 1))
  64. >>> Polygon(p1, p2)
  65. Segment2D(Point2D(0, 0), Point2D(1, 0))
  66. >>> Polygon(p1, p2, p5)
  67. Segment2D(Point2D(0, 0), Point2D(3, 0))
  68. The area of a polygon is calculated as positive when vertices are
  69. traversed in a ccw direction. When the sides of a polygon cross the
  70. area will have positive and negative contributions. The following
  71. defines a Z shape where the bottom right connects back to the top
  72. left.
  73. >>> Polygon((0, 2), (2, 2), (0, 0), (2, 0)).area
  74. 0
  75. When the keyword `n` is used to define the number of sides of the
  76. Polygon then a RegularPolygon is created and the other arguments are
  77. interpreted as center, radius and rotation. The unrotated RegularPolygon
  78. will always have a vertex at Point(r, 0) where `r` is the radius of the
  79. circle that circumscribes the RegularPolygon. Its method `spin` can be
  80. used to increment that angle.
  81. >>> p = Polygon((0,0), 1, n=3)
  82. >>> p
  83. RegularPolygon(Point2D(0, 0), 1, 3, 0)
  84. >>> p.vertices[0]
  85. Point2D(1, 0)
  86. >>> p.args[0]
  87. Point2D(0, 0)
  88. >>> p.spin(pi/2)
  89. >>> p.vertices[0]
  90. Point2D(0, 1)
  91. """
  92. __slots__ = ()
  93. def __new__(cls, *args, n = 0, **kwargs):
  94. if n:
  95. args = list(args)
  96. # return a virtual polygon with n sides
  97. if len(args) == 2: # center, radius
  98. args.append(n)
  99. elif len(args) == 3: # center, radius, rotation
  100. args.insert(2, n)
  101. return RegularPolygon(*args, **kwargs)
  102. vertices = [Point(a, dim=2, **kwargs) for a in args]
  103. # remove consecutive duplicates
  104. nodup = []
  105. for p in vertices:
  106. if nodup and p == nodup[-1]:
  107. continue
  108. nodup.append(p)
  109. if len(nodup) > 1 and nodup[-1] == nodup[0]:
  110. nodup.pop() # last point was same as first
  111. # remove collinear points
  112. i = -3
  113. while i < len(nodup) - 3 and len(nodup) > 2:
  114. a, b, c = nodup[i], nodup[i + 1], nodup[i + 2]
  115. if Point.is_collinear(a, b, c):
  116. nodup.pop(i + 1)
  117. if a == c:
  118. nodup.pop(i)
  119. else:
  120. i += 1
  121. vertices = list(nodup)
  122. if len(vertices) > 3:
  123. return GeometryEntity.__new__(cls, *vertices, **kwargs)
  124. elif len(vertices) == 3:
  125. return Triangle(*vertices, **kwargs)
  126. elif len(vertices) == 2:
  127. return Segment(*vertices, **kwargs)
  128. else:
  129. return Point(*vertices, **kwargs)
  130. @property
  131. def area(self):
  132. """
  133. The area of the polygon.
  134. Notes
  135. =====
  136. The area calculation can be positive or negative based on the
  137. orientation of the points. If any side of the polygon crosses
  138. any other side, there will be areas having opposite signs.
  139. See Also
  140. ========
  141. sympy.geometry.ellipse.Ellipse.area
  142. Examples
  143. ========
  144. >>> from sympy import Point, Polygon
  145. >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)])
  146. >>> poly = Polygon(p1, p2, p3, p4)
  147. >>> poly.area
  148. 3
  149. In the Z shaped polygon (with the lower right connecting back
  150. to the upper left) the areas cancel out:
  151. >>> Z = Polygon((0, 1), (1, 1), (0, 0), (1, 0))
  152. >>> Z.area
  153. 0
  154. In the M shaped polygon, areas do not cancel because no side
  155. crosses any other (though there is a point of contact).
  156. >>> M = Polygon((0, 0), (0, 1), (2, 0), (3, 1), (3, 0))
  157. >>> M.area
  158. -3/2
  159. """
  160. area = 0
  161. args = self.args
  162. for i in range(len(args)):
  163. x1, y1 = args[i - 1].args
  164. x2, y2 = args[i].args
  165. area += x1*y2 - x2*y1
  166. return simplify(area) / 2
  167. @staticmethod
  168. def _is_clockwise(a, b, c):
  169. """Return True/False for cw/ccw orientation.
  170. Examples
  171. ========
  172. >>> from sympy import Point, Polygon
  173. >>> a, b, c = [Point(i) for i in [(0, 0), (1, 1), (1, 0)]]
  174. >>> Polygon._is_clockwise(a, b, c)
  175. True
  176. >>> Polygon._is_clockwise(a, c, b)
  177. False
  178. """
  179. ba = b - a
  180. ca = c - a
  181. t_area = simplify(ba.x*ca.y - ca.x*ba.y)
  182. res = t_area.is_nonpositive
  183. if res is None:
  184. raise ValueError("Can't determine orientation")
  185. return res
  186. @property
  187. def angles(self):
  188. """The internal angle at each vertex.
  189. Returns
  190. =======
  191. angles : dict
  192. A dictionary where each key is a vertex and each value is the
  193. internal angle at that vertex. The vertices are represented as
  194. Points.
  195. See Also
  196. ========
  197. sympy.geometry.point.Point, sympy.geometry.line.LinearEntity.angle_between
  198. Examples
  199. ========
  200. >>> from sympy import Point, Polygon
  201. >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)])
  202. >>> poly = Polygon(p1, p2, p3, p4)
  203. >>> poly.angles[p1]
  204. pi/2
  205. >>> poly.angles[p2]
  206. acos(-4*sqrt(17)/17)
  207. """
  208. args = self.vertices
  209. n = len(args)
  210. ret = {}
  211. for i in range(n):
  212. a, b, c = args[i - 2], args[i - 1], args[i]
  213. reflex_ang = Ray(b, a).angle_between(Ray(b, c))
  214. if self._is_clockwise(a, b, c):
  215. ret[b] = 2*S.Pi - reflex_ang
  216. else:
  217. ret[b] = reflex_ang
  218. # internal sum should be pi*(n - 2), not pi*(n+2)
  219. # so if ratio is (n+2)/(n-2) > 1 it is wrong
  220. wrong = ((sum(ret.values())/S.Pi-1)/(n - 2) - 1).is_positive
  221. if wrong:
  222. two_pi = 2*S.Pi
  223. for b in ret:
  224. ret[b] = two_pi - ret[b]
  225. elif wrong is None:
  226. raise ValueError("could not determine Polygon orientation.")
  227. return ret
  228. @property
  229. def ambient_dimension(self):
  230. return self.vertices[0].ambient_dimension
  231. @property
  232. def perimeter(self):
  233. """The perimeter of the polygon.
  234. Returns
  235. =======
  236. perimeter : number or Basic instance
  237. See Also
  238. ========
  239. sympy.geometry.line.Segment.length
  240. Examples
  241. ========
  242. >>> from sympy import Point, Polygon
  243. >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)])
  244. >>> poly = Polygon(p1, p2, p3, p4)
  245. >>> poly.perimeter
  246. sqrt(17) + 7
  247. """
  248. p = 0
  249. args = self.vertices
  250. for i in range(len(args)):
  251. p += args[i - 1].distance(args[i])
  252. return simplify(p)
  253. @property
  254. def vertices(self):
  255. """The vertices of the polygon.
  256. Returns
  257. =======
  258. vertices : list of Points
  259. Notes
  260. =====
  261. When iterating over the vertices, it is more efficient to index self
  262. rather than to request the vertices and index them. Only use the
  263. vertices when you want to process all of them at once. This is even
  264. more important with RegularPolygons that calculate each vertex.
  265. See Also
  266. ========
  267. sympy.geometry.point.Point
  268. Examples
  269. ========
  270. >>> from sympy import Point, Polygon
  271. >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)])
  272. >>> poly = Polygon(p1, p2, p3, p4)
  273. >>> poly.vertices
  274. [Point2D(0, 0), Point2D(1, 0), Point2D(5, 1), Point2D(0, 1)]
  275. >>> poly.vertices[0]
  276. Point2D(0, 0)
  277. """
  278. return list(self.args)
  279. @property
  280. def centroid(self):
  281. """The centroid of the polygon.
  282. Returns
  283. =======
  284. centroid : Point
  285. See Also
  286. ========
  287. sympy.geometry.point.Point, sympy.geometry.util.centroid
  288. Examples
  289. ========
  290. >>> from sympy import Point, Polygon
  291. >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)])
  292. >>> poly = Polygon(p1, p2, p3, p4)
  293. >>> poly.centroid
  294. Point2D(31/18, 11/18)
  295. """
  296. A = 1/(6*self.area)
  297. cx, cy = 0, 0
  298. args = self.args
  299. for i in range(len(args)):
  300. x1, y1 = args[i - 1].args
  301. x2, y2 = args[i].args
  302. v = x1*y2 - x2*y1
  303. cx += v*(x1 + x2)
  304. cy += v*(y1 + y2)
  305. return Point(simplify(A*cx), simplify(A*cy))
  306. def second_moment_of_area(self, point=None):
  307. """Returns the second moment and product moment of area of a two dimensional polygon.
  308. Parameters
  309. ==========
  310. point : Point, two-tuple of sympifyable objects, or None(default=None)
  311. point is the point about which second moment of area is to be found.
  312. If "point=None" it will be calculated about the axis passing through the
  313. centroid of the polygon.
  314. Returns
  315. =======
  316. I_xx, I_yy, I_xy : number or SymPy expression
  317. I_xx, I_yy are second moment of area of a two dimensional polygon.
  318. I_xy is product moment of area of a two dimensional polygon.
  319. Examples
  320. ========
  321. >>> from sympy import Polygon, symbols
  322. >>> a, b = symbols('a, b')
  323. >>> p1, p2, p3, p4, p5 = [(0, 0), (a, 0), (a, b), (0, b), (a/3, b/3)]
  324. >>> rectangle = Polygon(p1, p2, p3, p4)
  325. >>> rectangle.second_moment_of_area()
  326. (a*b**3/12, a**3*b/12, 0)
  327. >>> rectangle.second_moment_of_area(p5)
  328. (a*b**3/9, a**3*b/9, a**2*b**2/36)
  329. References
  330. ==========
  331. .. [1] https://en.wikipedia.org/wiki/Second_moment_of_area
  332. """
  333. I_xx, I_yy, I_xy = 0, 0, 0
  334. args = self.vertices
  335. for i in range(len(args)):
  336. x1, y1 = args[i-1].args
  337. x2, y2 = args[i].args
  338. v = x1*y2 - x2*y1
  339. I_xx += (y1**2 + y1*y2 + y2**2)*v
  340. I_yy += (x1**2 + x1*x2 + x2**2)*v
  341. I_xy += (x1*y2 + 2*x1*y1 + 2*x2*y2 + x2*y1)*v
  342. A = self.area
  343. c_x = self.centroid[0]
  344. c_y = self.centroid[1]
  345. # parallel axis theorem
  346. I_xx_c = (I_xx/12) - (A*(c_y**2))
  347. I_yy_c = (I_yy/12) - (A*(c_x**2))
  348. I_xy_c = (I_xy/24) - (A*(c_x*c_y))
  349. if point is None:
  350. return I_xx_c, I_yy_c, I_xy_c
  351. I_xx = (I_xx_c + A*((point[1]-c_y)**2))
  352. I_yy = (I_yy_c + A*((point[0]-c_x)**2))
  353. I_xy = (I_xy_c + A*((point[0]-c_x)*(point[1]-c_y)))
  354. return I_xx, I_yy, I_xy
  355. def first_moment_of_area(self, point=None):
  356. """
  357. Returns the first moment of area of a two-dimensional polygon with
  358. respect to a certain point of interest.
  359. First moment of area is a measure of the distribution of the area
  360. of a polygon in relation to an axis. The first moment of area of
  361. the entire polygon about its own centroid is always zero. Therefore,
  362. here it is calculated for an area, above or below a certain point
  363. of interest, that makes up a smaller portion of the polygon. This
  364. area is bounded by the point of interest and the extreme end
  365. (top or bottom) of the polygon. The first moment for this area is
  366. is then determined about the centroidal axis of the initial polygon.
  367. References
  368. ==========
  369. .. [1] https://skyciv.com/docs/tutorials/section-tutorials/calculating-the-statical-or-first-moment-of-area-of-beam-sections/?cc=BMD
  370. .. [2] https://mechanicalc.com/reference/cross-sections
  371. Parameters
  372. ==========
  373. point: Point, two-tuple of sympifyable objects, or None (default=None)
  374. point is the point above or below which the area of interest lies
  375. If ``point=None`` then the centroid acts as the point of interest.
  376. Returns
  377. =======
  378. Q_x, Q_y: number or SymPy expressions
  379. Q_x is the first moment of area about the x-axis
  380. Q_y is the first moment of area about the y-axis
  381. A negative sign indicates that the section modulus is
  382. determined for a section below (or left of) the centroidal axis
  383. Examples
  384. ========
  385. >>> from sympy import Point, Polygon
  386. >>> a, b = 50, 10
  387. >>> p1, p2, p3, p4 = [(0, b), (0, 0), (a, 0), (a, b)]
  388. >>> p = Polygon(p1, p2, p3, p4)
  389. >>> p.first_moment_of_area()
  390. (625, 3125)
  391. >>> p.first_moment_of_area(point=Point(30, 7))
  392. (525, 3000)
  393. """
  394. if point:
  395. xc, yc = self.centroid
  396. else:
  397. point = self.centroid
  398. xc, yc = point
  399. h_line = Line(point, slope=0)
  400. v_line = Line(point, slope=S.Infinity)
  401. h_poly = self.cut_section(h_line)
  402. v_poly = self.cut_section(v_line)
  403. poly_1 = h_poly[0] if h_poly[0].area <= h_poly[1].area else h_poly[1]
  404. poly_2 = v_poly[0] if v_poly[0].area <= v_poly[1].area else v_poly[1]
  405. Q_x = (poly_1.centroid.y - yc)*poly_1.area
  406. Q_y = (poly_2.centroid.x - xc)*poly_2.area
  407. return Q_x, Q_y
  408. def polar_second_moment_of_area(self):
  409. """Returns the polar modulus of a two-dimensional polygon
  410. It is a constituent of the second moment of area, linked through
  411. the perpendicular axis theorem. While the planar second moment of
  412. area describes an object's resistance to deflection (bending) when
  413. subjected to a force applied to a plane parallel to the central
  414. axis, the polar second moment of area describes an object's
  415. resistance to deflection when subjected to a moment applied in a
  416. plane perpendicular to the object's central axis (i.e. parallel to
  417. the cross-section)
  418. Examples
  419. ========
  420. >>> from sympy import Polygon, symbols
  421. >>> a, b = symbols('a, b')
  422. >>> rectangle = Polygon((0, 0), (a, 0), (a, b), (0, b))
  423. >>> rectangle.polar_second_moment_of_area()
  424. a**3*b/12 + a*b**3/12
  425. References
  426. ==========
  427. .. [1] https://en.wikipedia.org/wiki/Polar_moment_of_inertia
  428. """
  429. second_moment = self.second_moment_of_area()
  430. return second_moment[0] + second_moment[1]
  431. def section_modulus(self, point=None):
  432. """Returns a tuple with the section modulus of a two-dimensional
  433. polygon.
  434. Section modulus is a geometric property of a polygon defined as the
  435. ratio of second moment of area to the distance of the extreme end of
  436. the polygon from the centroidal axis.
  437. Parameters
  438. ==========
  439. point : Point, two-tuple of sympifyable objects, or None(default=None)
  440. point is the point at which section modulus is to be found.
  441. If "point=None" it will be calculated for the point farthest from the
  442. centroidal axis of the polygon.
  443. Returns
  444. =======
  445. S_x, S_y: numbers or SymPy expressions
  446. S_x is the section modulus with respect to the x-axis
  447. S_y is the section modulus with respect to the y-axis
  448. A negative sign indicates that the section modulus is
  449. determined for a point below the centroidal axis
  450. Examples
  451. ========
  452. >>> from sympy import symbols, Polygon, Point
  453. >>> a, b = symbols('a, b', positive=True)
  454. >>> rectangle = Polygon((0, 0), (a, 0), (a, b), (0, b))
  455. >>> rectangle.section_modulus()
  456. (a*b**2/6, a**2*b/6)
  457. >>> rectangle.section_modulus(Point(a/4, b/4))
  458. (-a*b**2/3, -a**2*b/3)
  459. References
  460. ==========
  461. .. [1] https://en.wikipedia.org/wiki/Section_modulus
  462. """
  463. x_c, y_c = self.centroid
  464. if point is None:
  465. # taking x and y as maximum distances from centroid
  466. x_min, y_min, x_max, y_max = self.bounds
  467. y = max(y_c - y_min, y_max - y_c)
  468. x = max(x_c - x_min, x_max - x_c)
  469. else:
  470. # taking x and y as distances of the given point from the centroid
  471. y = point.y - y_c
  472. x = point.x - x_c
  473. second_moment= self.second_moment_of_area()
  474. S_x = second_moment[0]/y
  475. S_y = second_moment[1]/x
  476. return S_x, S_y
  477. @property
  478. def sides(self):
  479. """The directed line segments that form the sides of the polygon.
  480. Returns
  481. =======
  482. sides : list of sides
  483. Each side is a directed Segment.
  484. See Also
  485. ========
  486. sympy.geometry.point.Point, sympy.geometry.line.Segment
  487. Examples
  488. ========
  489. >>> from sympy import Point, Polygon
  490. >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)])
  491. >>> poly = Polygon(p1, p2, p3, p4)
  492. >>> poly.sides
  493. [Segment2D(Point2D(0, 0), Point2D(1, 0)),
  494. Segment2D(Point2D(1, 0), Point2D(5, 1)),
  495. Segment2D(Point2D(5, 1), Point2D(0, 1)), Segment2D(Point2D(0, 1), Point2D(0, 0))]
  496. """
  497. res = []
  498. args = self.vertices
  499. for i in range(-len(args), 0):
  500. res.append(Segment(args[i], args[i + 1]))
  501. return res
  502. @property
  503. def bounds(self):
  504. """Return a tuple (xmin, ymin, xmax, ymax) representing the bounding
  505. rectangle for the geometric figure.
  506. """
  507. verts = self.vertices
  508. xs = [p.x for p in verts]
  509. ys = [p.y for p in verts]
  510. return (min(xs), min(ys), max(xs), max(ys))
  511. def is_convex(self):
  512. """Is the polygon convex?
  513. A polygon is convex if all its interior angles are less than 180
  514. degrees and there are no intersections between sides.
  515. Returns
  516. =======
  517. is_convex : boolean
  518. True if this polygon is convex, False otherwise.
  519. See Also
  520. ========
  521. sympy.geometry.util.convex_hull
  522. Examples
  523. ========
  524. >>> from sympy import Point, Polygon
  525. >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)])
  526. >>> poly = Polygon(p1, p2, p3, p4)
  527. >>> poly.is_convex()
  528. True
  529. """
  530. # Determine orientation of points
  531. args = self.vertices
  532. cw = self._is_clockwise(args[-2], args[-1], args[0])
  533. for i in range(1, len(args)):
  534. if cw ^ self._is_clockwise(args[i - 2], args[i - 1], args[i]):
  535. return False
  536. # check for intersecting sides
  537. sides = self.sides
  538. for i, si in enumerate(sides):
  539. pts = si.args
  540. # exclude the sides connected to si
  541. for j in range(1 if i == len(sides) - 1 else 0, i - 1):
  542. sj = sides[j]
  543. if sj.p1 not in pts and sj.p2 not in pts:
  544. hit = si.intersection(sj)
  545. if hit:
  546. return False
  547. return True
  548. def encloses_point(self, p):
  549. """
  550. Return True if p is enclosed by (is inside of) self.
  551. Notes
  552. =====
  553. Being on the border of self is considered False.
  554. Parameters
  555. ==========
  556. p : Point
  557. Returns
  558. =======
  559. encloses_point : True, False or None
  560. See Also
  561. ========
  562. sympy.geometry.point.Point, sympy.geometry.ellipse.Ellipse.encloses_point
  563. Examples
  564. ========
  565. >>> from sympy import Polygon, Point
  566. >>> p = Polygon((0, 0), (4, 0), (4, 4))
  567. >>> p.encloses_point(Point(2, 1))
  568. True
  569. >>> p.encloses_point(Point(2, 2))
  570. False
  571. >>> p.encloses_point(Point(5, 5))
  572. False
  573. References
  574. ==========
  575. .. [1] https://paulbourke.net/geometry/polygonmesh/#insidepoly
  576. """
  577. p = Point(p, dim=2)
  578. if p in self.vertices or any(p in s for s in self.sides):
  579. return False
  580. # move to p, checking that the result is numeric
  581. lit = []
  582. for v in self.vertices:
  583. lit.append(v - p) # the difference is simplified
  584. if lit[-1].free_symbols:
  585. return None
  586. poly = Polygon(*lit)
  587. # polygon closure is assumed in the following test but Polygon removes duplicate pts so
  588. # the last point has to be added so all sides are computed. Using Polygon.sides is
  589. # not good since Segments are unordered.
  590. args = poly.args
  591. indices = list(range(-len(args), 1))
  592. if poly.is_convex():
  593. orientation = None
  594. for i in indices:
  595. a = args[i]
  596. b = args[i + 1]
  597. test = ((-a.y)*(b.x - a.x) - (-a.x)*(b.y - a.y)).is_negative
  598. if orientation is None:
  599. orientation = test
  600. elif test is not orientation:
  601. return False
  602. return True
  603. hit_odd = False
  604. p1x, p1y = args[0].args
  605. for i in indices[1:]:
  606. p2x, p2y = args[i].args
  607. if 0 > min(p1y, p2y):
  608. if 0 <= max(p1y, p2y):
  609. if 0 <= max(p1x, p2x):
  610. if p1y != p2y:
  611. xinters = (-p1y)*(p2x - p1x)/(p2y - p1y) + p1x
  612. if p1x == p2x or 0 <= xinters:
  613. hit_odd = not hit_odd
  614. p1x, p1y = p2x, p2y
  615. return hit_odd
  616. def arbitrary_point(self, parameter='t'):
  617. """A parameterized point on the polygon.
  618. The parameter, varying from 0 to 1, assigns points to the position on
  619. the perimeter that is that fraction of the total perimeter. So the
  620. point evaluated at t=1/2 would return the point from the first vertex
  621. that is 1/2 way around the polygon.
  622. Parameters
  623. ==========
  624. parameter : str, optional
  625. Default value is 't'.
  626. Returns
  627. =======
  628. arbitrary_point : Point
  629. Raises
  630. ======
  631. ValueError
  632. When `parameter` already appears in the Polygon's definition.
  633. See Also
  634. ========
  635. sympy.geometry.point.Point
  636. Examples
  637. ========
  638. >>> from sympy import Polygon, Symbol
  639. >>> t = Symbol('t', real=True)
  640. >>> tri = Polygon((0, 0), (1, 0), (1, 1))
  641. >>> p = tri.arbitrary_point('t')
  642. >>> perimeter = tri.perimeter
  643. >>> s1, s2 = [s.length for s in tri.sides[:2]]
  644. >>> p.subs(t, (s1 + s2/2)/perimeter)
  645. Point2D(1, 1/2)
  646. """
  647. t = _symbol(parameter, real=True)
  648. if t.name in (f.name for f in self.free_symbols):
  649. raise ValueError('Symbol %s already appears in object and cannot be used as a parameter.' % t.name)
  650. sides = []
  651. perimeter = self.perimeter
  652. perim_fraction_start = 0
  653. for s in self.sides:
  654. side_perim_fraction = s.length/perimeter
  655. perim_fraction_end = perim_fraction_start + side_perim_fraction
  656. pt = s.arbitrary_point(parameter).subs(
  657. t, (t - perim_fraction_start)/side_perim_fraction)
  658. sides.append(
  659. (pt, (And(perim_fraction_start <= t, t < perim_fraction_end))))
  660. perim_fraction_start = perim_fraction_end
  661. return Piecewise(*sides)
  662. def parameter_value(self, other, t):
  663. if not isinstance(other,GeometryEntity):
  664. other = Point(other, dim=self.ambient_dimension)
  665. if not isinstance(other,Point):
  666. raise ValueError("other must be a point")
  667. if other.free_symbols:
  668. raise NotImplementedError('non-numeric coordinates')
  669. unknown = False
  670. p = self.arbitrary_point(T)
  671. for pt, cond in p.args:
  672. sol = solve(pt - other, T, dict=True)
  673. if not sol:
  674. continue
  675. value = sol[0][T]
  676. if simplify(cond.subs(T, value)) == True:
  677. return {t: value}
  678. unknown = True
  679. if unknown:
  680. raise ValueError("Given point may not be on %s" % func_name(self))
  681. raise ValueError("Given point is not on %s" % func_name(self))
  682. def plot_interval(self, parameter='t'):
  683. """The plot interval for the default geometric plot of the polygon.
  684. Parameters
  685. ==========
  686. parameter : str, optional
  687. Default value is 't'.
  688. Returns
  689. =======
  690. plot_interval : list (plot interval)
  691. [parameter, lower_bound, upper_bound]
  692. Examples
  693. ========
  694. >>> from sympy import Polygon
  695. >>> p = Polygon((0, 0), (1, 0), (1, 1))
  696. >>> p.plot_interval()
  697. [t, 0, 1]
  698. """
  699. t = Symbol(parameter, real=True)
  700. return [t, 0, 1]
  701. def intersection(self, o):
  702. """The intersection of polygon and geometry entity.
  703. The intersection may be empty and can contain individual Points and
  704. complete Line Segments.
  705. Parameters
  706. ==========
  707. other: GeometryEntity
  708. Returns
  709. =======
  710. intersection : list
  711. The list of Segments and Points
  712. See Also
  713. ========
  714. sympy.geometry.point.Point, sympy.geometry.line.Segment
  715. Examples
  716. ========
  717. >>> from sympy import Point, Polygon, Line
  718. >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)])
  719. >>> poly1 = Polygon(p1, p2, p3, p4)
  720. >>> p5, p6, p7 = map(Point, [(3, 2), (1, -1), (0, 2)])
  721. >>> poly2 = Polygon(p5, p6, p7)
  722. >>> poly1.intersection(poly2)
  723. [Point2D(1/3, 1), Point2D(2/3, 0), Point2D(9/5, 1/5), Point2D(7/3, 1)]
  724. >>> poly1.intersection(Line(p1, p2))
  725. [Segment2D(Point2D(0, 0), Point2D(1, 0))]
  726. >>> poly1.intersection(p1)
  727. [Point2D(0, 0)]
  728. """
  729. intersection_result = []
  730. k = o.sides if isinstance(o, Polygon) else [o]
  731. for side in self.sides:
  732. for side1 in k:
  733. intersection_result.extend(side.intersection(side1))
  734. intersection_result = list(uniq(intersection_result))
  735. points = [entity for entity in intersection_result if isinstance(entity, Point)]
  736. segments = [entity for entity in intersection_result if isinstance(entity, Segment)]
  737. if points and segments:
  738. points_in_segments = list(uniq([point for point in points for segment in segments if point in segment]))
  739. if points_in_segments:
  740. for i in points_in_segments:
  741. points.remove(i)
  742. return list(ordered(segments + points))
  743. else:
  744. return list(ordered(intersection_result))
  745. def cut_section(self, line):
  746. """
  747. Returns a tuple of two polygon segments that lie above and below
  748. the intersecting line respectively.
  749. Parameters
  750. ==========
  751. line: Line object of geometry module
  752. line which cuts the Polygon. The part of the Polygon that lies
  753. above and below this line is returned.
  754. Returns
  755. =======
  756. upper_polygon, lower_polygon: Polygon objects or None
  757. upper_polygon is the polygon that lies above the given line.
  758. lower_polygon is the polygon that lies below the given line.
  759. upper_polygon and lower polygon are ``None`` when no polygon
  760. exists above the line or below the line.
  761. Raises
  762. ======
  763. ValueError: When the line does not intersect the polygon
  764. Examples
  765. ========
  766. >>> from sympy import Polygon, Line
  767. >>> a, b = 20, 10
  768. >>> p1, p2, p3, p4 = [(0, b), (0, 0), (a, 0), (a, b)]
  769. >>> rectangle = Polygon(p1, p2, p3, p4)
  770. >>> t = rectangle.cut_section(Line((0, 5), slope=0))
  771. >>> t
  772. (Polygon(Point2D(0, 10), Point2D(0, 5), Point2D(20, 5), Point2D(20, 10)),
  773. Polygon(Point2D(0, 5), Point2D(0, 0), Point2D(20, 0), Point2D(20, 5)))
  774. >>> upper_segment, lower_segment = t
  775. >>> upper_segment.area
  776. 100
  777. >>> upper_segment.centroid
  778. Point2D(10, 15/2)
  779. >>> lower_segment.centroid
  780. Point2D(10, 5/2)
  781. References
  782. ==========
  783. .. [1] https://github.com/sympy/sympy/wiki/A-method-to-return-a-cut-section-of-any-polygon-geometry
  784. """
  785. intersection_points = self.intersection(line)
  786. if not intersection_points:
  787. raise ValueError("This line does not intersect the polygon")
  788. points = list(self.vertices)
  789. points.append(points[0])
  790. eq = line.equation(x, y)
  791. # considering equation of line to be `ax +by + c`
  792. a = eq.coeff(x)
  793. b = eq.coeff(y)
  794. upper_vertices = []
  795. lower_vertices = []
  796. # prev is true when previous point is above the line
  797. prev = True
  798. prev_point = None
  799. for point in points:
  800. # when coefficient of y is 0, right side of the line is
  801. # considered
  802. compare = eq.subs({x: point.x, y: point.y})/b if b \
  803. else eq.subs(x, point.x)/a
  804. # if point lies above line
  805. if compare > 0:
  806. if not prev:
  807. # if previous point lies below the line, the intersection
  808. # point of the polygon edge and the line has to be included
  809. edge = Line(point, prev_point)
  810. new_point = edge.intersection(line)
  811. upper_vertices.append(new_point[0])
  812. lower_vertices.append(new_point[0])
  813. upper_vertices.append(point)
  814. prev = True
  815. else:
  816. if prev and prev_point:
  817. edge = Line(point, prev_point)
  818. new_point = edge.intersection(line)
  819. upper_vertices.append(new_point[0])
  820. lower_vertices.append(new_point[0])
  821. lower_vertices.append(point)
  822. prev = False
  823. prev_point = point
  824. upper_polygon, lower_polygon = None, None
  825. if upper_vertices and isinstance(Polygon(*upper_vertices), Polygon):
  826. upper_polygon = Polygon(*upper_vertices)
  827. if lower_vertices and isinstance(Polygon(*lower_vertices), Polygon):
  828. lower_polygon = Polygon(*lower_vertices)
  829. return upper_polygon, lower_polygon
  830. def distance(self, o):
  831. """
  832. Returns the shortest distance between self and o.
  833. If o is a point, then self does not need to be convex.
  834. If o is another polygon self and o must be convex.
  835. Examples
  836. ========
  837. >>> from sympy import Point, Polygon, RegularPolygon
  838. >>> p1, p2 = map(Point, [(0, 0), (7, 5)])
  839. >>> poly = Polygon(*RegularPolygon(p1, 1, 3).vertices)
  840. >>> poly.distance(p2)
  841. sqrt(61)
  842. """
  843. if isinstance(o, Point):
  844. dist = oo
  845. for side in self.sides:
  846. current = side.distance(o)
  847. if current == 0:
  848. return S.Zero
  849. elif current < dist:
  850. dist = current
  851. return dist
  852. elif isinstance(o, Polygon) and self.is_convex() and o.is_convex():
  853. return self._do_poly_distance(o)
  854. raise NotImplementedError()
  855. def _do_poly_distance(self, e2):
  856. """
  857. Calculates the least distance between the exteriors of two
  858. convex polygons e1 and e2. Does not check for the convexity
  859. of the polygons as this is checked by Polygon.distance.
  860. Notes
  861. =====
  862. - Prints a warning if the two polygons possibly intersect as the return
  863. value will not be valid in such a case. For a more through test of
  864. intersection use intersection().
  865. See Also
  866. ========
  867. sympy.geometry.point.Point.distance
  868. Examples
  869. ========
  870. >>> from sympy import Point, Polygon
  871. >>> square = Polygon(Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0))
  872. >>> triangle = Polygon(Point(1, 2), Point(2, 2), Point(2, 1))
  873. >>> square._do_poly_distance(triangle)
  874. sqrt(2)/2
  875. Description of method used
  876. ==========================
  877. Method:
  878. [1] https://web.archive.org/web/20150509035744/http://cgm.cs.mcgill.ca/~orm/mind2p.html
  879. Uses rotating calipers:
  880. [2] https://en.wikipedia.org/wiki/Rotating_calipers
  881. and antipodal points:
  882. [3] https://en.wikipedia.org/wiki/Antipodal_point
  883. """
  884. e1 = self
  885. '''Tests for a possible intersection between the polygons and outputs a warning'''
  886. e1_center = e1.centroid
  887. e2_center = e2.centroid
  888. e1_max_radius = S.Zero
  889. e2_max_radius = S.Zero
  890. for vertex in e1.vertices:
  891. r = Point.distance(e1_center, vertex)
  892. if e1_max_radius < r:
  893. e1_max_radius = r
  894. for vertex in e2.vertices:
  895. r = Point.distance(e2_center, vertex)
  896. if e2_max_radius < r:
  897. e2_max_radius = r
  898. center_dist = Point.distance(e1_center, e2_center)
  899. if center_dist <= e1_max_radius + e2_max_radius:
  900. warnings.warn("Polygons may intersect producing erroneous output",
  901. stacklevel=3)
  902. '''
  903. Find the upper rightmost vertex of e1 and the lowest leftmost vertex of e2
  904. '''
  905. e1_ymax = Point(0, -oo)
  906. e2_ymin = Point(0, oo)
  907. for vertex in e1.vertices:
  908. if vertex.y > e1_ymax.y or (vertex.y == e1_ymax.y and vertex.x > e1_ymax.x):
  909. e1_ymax = vertex
  910. for vertex in e2.vertices:
  911. if vertex.y < e2_ymin.y or (vertex.y == e2_ymin.y and vertex.x < e2_ymin.x):
  912. e2_ymin = vertex
  913. min_dist = Point.distance(e1_ymax, e2_ymin)
  914. '''
  915. Produce a dictionary with vertices of e1 as the keys and, for each vertex, the points
  916. to which the vertex is connected as its value. The same is then done for e2.
  917. '''
  918. e1_connections = {}
  919. e2_connections = {}
  920. for side in e1.sides:
  921. if side.p1 in e1_connections:
  922. e1_connections[side.p1].append(side.p2)
  923. else:
  924. e1_connections[side.p1] = [side.p2]
  925. if side.p2 in e1_connections:
  926. e1_connections[side.p2].append(side.p1)
  927. else:
  928. e1_connections[side.p2] = [side.p1]
  929. for side in e2.sides:
  930. if side.p1 in e2_connections:
  931. e2_connections[side.p1].append(side.p2)
  932. else:
  933. e2_connections[side.p1] = [side.p2]
  934. if side.p2 in e2_connections:
  935. e2_connections[side.p2].append(side.p1)
  936. else:
  937. e2_connections[side.p2] = [side.p1]
  938. e1_current = e1_ymax
  939. e2_current = e2_ymin
  940. support_line = Line(Point(S.Zero, S.Zero), Point(S.One, S.Zero))
  941. '''
  942. Determine which point in e1 and e2 will be selected after e2_ymin and e1_ymax,
  943. this information combined with the above produced dictionaries determines the
  944. path that will be taken around the polygons
  945. '''
  946. point1 = e1_connections[e1_ymax][0]
  947. point2 = e1_connections[e1_ymax][1]
  948. angle1 = support_line.angle_between(Line(e1_ymax, point1))
  949. angle2 = support_line.angle_between(Line(e1_ymax, point2))
  950. if angle1 < angle2:
  951. e1_next = point1
  952. elif angle2 < angle1:
  953. e1_next = point2
  954. elif Point.distance(e1_ymax, point1) > Point.distance(e1_ymax, point2):
  955. e1_next = point2
  956. else:
  957. e1_next = point1
  958. point1 = e2_connections[e2_ymin][0]
  959. point2 = e2_connections[e2_ymin][1]
  960. angle1 = support_line.angle_between(Line(e2_ymin, point1))
  961. angle2 = support_line.angle_between(Line(e2_ymin, point2))
  962. if angle1 > angle2:
  963. e2_next = point1
  964. elif angle2 > angle1:
  965. e2_next = point2
  966. elif Point.distance(e2_ymin, point1) > Point.distance(e2_ymin, point2):
  967. e2_next = point2
  968. else:
  969. e2_next = point1
  970. '''
  971. Loop which determines the distance between anti-podal pairs and updates the
  972. minimum distance accordingly. It repeats until it reaches the starting position.
  973. '''
  974. while True:
  975. e1_angle = support_line.angle_between(Line(e1_current, e1_next))
  976. e2_angle = pi - support_line.angle_between(Line(
  977. e2_current, e2_next))
  978. if (e1_angle < e2_angle) is True:
  979. support_line = Line(e1_current, e1_next)
  980. e1_segment = Segment(e1_current, e1_next)
  981. min_dist_current = e1_segment.distance(e2_current)
  982. if min_dist_current.evalf() < min_dist.evalf():
  983. min_dist = min_dist_current
  984. if e1_connections[e1_next][0] != e1_current:
  985. e1_current = e1_next
  986. e1_next = e1_connections[e1_next][0]
  987. else:
  988. e1_current = e1_next
  989. e1_next = e1_connections[e1_next][1]
  990. elif (e1_angle > e2_angle) is True:
  991. support_line = Line(e2_next, e2_current)
  992. e2_segment = Segment(e2_current, e2_next)
  993. min_dist_current = e2_segment.distance(e1_current)
  994. if min_dist_current.evalf() < min_dist.evalf():
  995. min_dist = min_dist_current
  996. if e2_connections[e2_next][0] != e2_current:
  997. e2_current = e2_next
  998. e2_next = e2_connections[e2_next][0]
  999. else:
  1000. e2_current = e2_next
  1001. e2_next = e2_connections[e2_next][1]
  1002. else:
  1003. support_line = Line(e1_current, e1_next)
  1004. e1_segment = Segment(e1_current, e1_next)
  1005. e2_segment = Segment(e2_current, e2_next)
  1006. min1 = e1_segment.distance(e2_next)
  1007. min2 = e2_segment.distance(e1_next)
  1008. min_dist_current = min(min1, min2)
  1009. if min_dist_current.evalf() < min_dist.evalf():
  1010. min_dist = min_dist_current
  1011. if e1_connections[e1_next][0] != e1_current:
  1012. e1_current = e1_next
  1013. e1_next = e1_connections[e1_next][0]
  1014. else:
  1015. e1_current = e1_next
  1016. e1_next = e1_connections[e1_next][1]
  1017. if e2_connections[e2_next][0] != e2_current:
  1018. e2_current = e2_next
  1019. e2_next = e2_connections[e2_next][0]
  1020. else:
  1021. e2_current = e2_next
  1022. e2_next = e2_connections[e2_next][1]
  1023. if e1_current == e1_ymax and e2_current == e2_ymin:
  1024. break
  1025. return min_dist
  1026. def _svg(self, scale_factor=1., fill_color="#66cc99"):
  1027. """Returns SVG path element for the Polygon.
  1028. Parameters
  1029. ==========
  1030. scale_factor : float
  1031. Multiplication factor for the SVG stroke-width. Default is 1.
  1032. fill_color : str, optional
  1033. Hex string for fill color. Default is "#66cc99".
  1034. """
  1035. verts = map(N, self.vertices)
  1036. coords = ["{},{}".format(p.x, p.y) for p in verts]
  1037. path = "M {} L {} z".format(coords[0], " L ".join(coords[1:]))
  1038. return (
  1039. '<path fill-rule="evenodd" fill="{2}" stroke="#555555" '
  1040. 'stroke-width="{0}" opacity="0.6" d="{1}" />'
  1041. ).format(2. * scale_factor, path, fill_color)
  1042. def _hashable_content(self):
  1043. D = {}
  1044. def ref_list(point_list):
  1045. kee = {}
  1046. for i, p in enumerate(ordered(set(point_list))):
  1047. kee[p] = i
  1048. D[i] = p
  1049. return [kee[p] for p in point_list]
  1050. S1 = ref_list(self.args)
  1051. r_nor = rotate_left(S1, least_rotation(S1))
  1052. S2 = ref_list(list(reversed(self.args)))
  1053. r_rev = rotate_left(S2, least_rotation(S2))
  1054. if r_nor < r_rev:
  1055. r = r_nor
  1056. else:
  1057. r = r_rev
  1058. canonical_args = [ D[order] for order in r ]
  1059. return tuple(canonical_args)
  1060. def __contains__(self, o):
  1061. """
  1062. Return True if o is contained within the boundary lines of self.altitudes
  1063. Parameters
  1064. ==========
  1065. other : GeometryEntity
  1066. Returns
  1067. =======
  1068. contained in : bool
  1069. The points (and sides, if applicable) are contained in self.
  1070. See Also
  1071. ========
  1072. sympy.geometry.entity.GeometryEntity.encloses
  1073. Examples
  1074. ========
  1075. >>> from sympy import Line, Segment, Point
  1076. >>> p = Point(0, 0)
  1077. >>> q = Point(1, 1)
  1078. >>> s = Segment(p, q*2)
  1079. >>> l = Line(p, q)
  1080. >>> p in q
  1081. False
  1082. >>> p in s
  1083. True
  1084. >>> q*3 in s
  1085. False
  1086. >>> s in l
  1087. True
  1088. """
  1089. if isinstance(o, Polygon):
  1090. return self == o
  1091. elif isinstance(o, Segment):
  1092. return any(o in s for s in self.sides)
  1093. elif isinstance(o, Point):
  1094. if o in self.vertices:
  1095. return True
  1096. for side in self.sides:
  1097. if o in side:
  1098. return True
  1099. return False
  1100. def bisectors(p, prec=None):
  1101. """Returns angle bisectors of a polygon. If prec is given
  1102. then approximate the point defining the ray to that precision.
  1103. The distance between the points defining the bisector ray is 1.
  1104. Examples
  1105. ========
  1106. >>> from sympy import Polygon, Point
  1107. >>> p = Polygon(Point(0, 0), Point(2, 0), Point(1, 1), Point(0, 3))
  1108. >>> p.bisectors(2)
  1109. {Point2D(0, 0): Ray2D(Point2D(0, 0), Point2D(0.71, 0.71)),
  1110. Point2D(0, 3): Ray2D(Point2D(0, 3), Point2D(0.23, 2.0)),
  1111. Point2D(1, 1): Ray2D(Point2D(1, 1), Point2D(0.19, 0.42)),
  1112. Point2D(2, 0): Ray2D(Point2D(2, 0), Point2D(1.1, 0.38))}
  1113. """
  1114. b = {}
  1115. pts = list(p.args)
  1116. pts.append(pts[0]) # close it
  1117. cw = Polygon._is_clockwise(*pts[:3])
  1118. if cw:
  1119. pts = list(reversed(pts))
  1120. for v, a in p.angles.items():
  1121. i = pts.index(v)
  1122. p1, p2 = Point._normalize_dimension(pts[i], pts[i + 1])
  1123. ray = Ray(p1, p2).rotate(a/2, v)
  1124. dir = ray.direction
  1125. ray = Ray(ray.p1, ray.p1 + dir/dir.distance((0, 0)))
  1126. if prec is not None:
  1127. ray = Ray(ray.p1, ray.p2.n(prec))
  1128. b[v] = ray
  1129. return b
  1130. class RegularPolygon(Polygon):
  1131. """
  1132. A regular polygon.
  1133. Such a polygon has all internal angles equal and all sides the same length.
  1134. Parameters
  1135. ==========
  1136. center : Point
  1137. radius : number or Basic instance
  1138. The distance from the center to a vertex
  1139. n : int
  1140. The number of sides
  1141. Attributes
  1142. ==========
  1143. vertices
  1144. center
  1145. radius
  1146. rotation
  1147. apothem
  1148. interior_angle
  1149. exterior_angle
  1150. circumcircle
  1151. incircle
  1152. angles
  1153. Raises
  1154. ======
  1155. GeometryError
  1156. If the `center` is not a Point, or the `radius` is not a number or Basic
  1157. instance, or the number of sides, `n`, is less than three.
  1158. Notes
  1159. =====
  1160. A RegularPolygon can be instantiated with Polygon with the kwarg n.
  1161. Regular polygons are instantiated with a center, radius, number of sides
  1162. and a rotation angle. Whereas the arguments of a Polygon are vertices, the
  1163. vertices of the RegularPolygon must be obtained with the vertices method.
  1164. See Also
  1165. ========
  1166. sympy.geometry.point.Point, Polygon
  1167. Examples
  1168. ========
  1169. >>> from sympy import RegularPolygon, Point
  1170. >>> r = RegularPolygon(Point(0, 0), 5, 3)
  1171. >>> r
  1172. RegularPolygon(Point2D(0, 0), 5, 3, 0)
  1173. >>> r.vertices[0]
  1174. Point2D(5, 0)
  1175. """
  1176. __slots__ = ('_n', '_center', '_radius', '_rot')
  1177. def __new__(self, c, r, n, rot=0, **kwargs):
  1178. r, n, rot = map(sympify, (r, n, rot))
  1179. c = Point(c, dim=2, **kwargs)
  1180. if not isinstance(r, Expr):
  1181. raise GeometryError("r must be an Expr object, not %s" % r)
  1182. if n.is_Number:
  1183. as_int(n) # let an error raise if necessary
  1184. if n < 3:
  1185. raise GeometryError("n must be a >= 3, not %s" % n)
  1186. obj = GeometryEntity.__new__(self, c, r, n, **kwargs)
  1187. obj._n = n
  1188. obj._center = c
  1189. obj._radius = r
  1190. obj._rot = rot % (2*S.Pi/n) if rot.is_number else rot
  1191. return obj
  1192. def _eval_evalf(self, prec=15, **options):
  1193. c, r, n, a = self.args
  1194. dps = prec_to_dps(prec)
  1195. c, r, a = [i.evalf(n=dps, **options) for i in (c, r, a)]
  1196. return self.func(c, r, n, a)
  1197. @property
  1198. def args(self):
  1199. """
  1200. Returns the center point, the radius,
  1201. the number of sides, and the orientation angle.
  1202. Examples
  1203. ========
  1204. >>> from sympy import RegularPolygon, Point
  1205. >>> r = RegularPolygon(Point(0, 0), 5, 3)
  1206. >>> r.args
  1207. (Point2D(0, 0), 5, 3, 0)
  1208. """
  1209. return self._center, self._radius, self._n, self._rot
  1210. def __str__(self):
  1211. return 'RegularPolygon(%s, %s, %s, %s)' % tuple(self.args)
  1212. def __repr__(self):
  1213. return 'RegularPolygon(%s, %s, %s, %s)' % tuple(self.args)
  1214. @property
  1215. def area(self):
  1216. """Returns the area.
  1217. Examples
  1218. ========
  1219. >>> from sympy import RegularPolygon
  1220. >>> square = RegularPolygon((0, 0), 1, 4)
  1221. >>> square.area
  1222. 2
  1223. >>> _ == square.length**2
  1224. True
  1225. """
  1226. c, r, n, rot = self.args
  1227. return sign(r)*n*self.length**2/(4*tan(pi/n))
  1228. @property
  1229. def length(self):
  1230. """Returns the length of the sides.
  1231. The half-length of the side and the apothem form two legs
  1232. of a right triangle whose hypotenuse is the radius of the
  1233. regular polygon.
  1234. Examples
  1235. ========
  1236. >>> from sympy import RegularPolygon
  1237. >>> from sympy import sqrt
  1238. >>> s = square_in_unit_circle = RegularPolygon((0, 0), 1, 4)
  1239. >>> s.length
  1240. sqrt(2)
  1241. >>> sqrt((_/2)**2 + s.apothem**2) == s.radius
  1242. True
  1243. """
  1244. return self.radius*2*sin(pi/self._n)
  1245. @property
  1246. def center(self):
  1247. """The center of the RegularPolygon
  1248. This is also the center of the circumscribing circle.
  1249. Returns
  1250. =======
  1251. center : Point
  1252. See Also
  1253. ========
  1254. sympy.geometry.point.Point, sympy.geometry.ellipse.Ellipse.center
  1255. Examples
  1256. ========
  1257. >>> from sympy import RegularPolygon, Point
  1258. >>> rp = RegularPolygon(Point(0, 0), 5, 4)
  1259. >>> rp.center
  1260. Point2D(0, 0)
  1261. """
  1262. return self._center
  1263. centroid = center
  1264. @property
  1265. def circumcenter(self):
  1266. """
  1267. Alias for center.
  1268. Examples
  1269. ========
  1270. >>> from sympy import RegularPolygon, Point
  1271. >>> rp = RegularPolygon(Point(0, 0), 5, 4)
  1272. >>> rp.circumcenter
  1273. Point2D(0, 0)
  1274. """
  1275. return self.center
  1276. @property
  1277. def radius(self):
  1278. """Radius of the RegularPolygon
  1279. This is also the radius of the circumscribing circle.
  1280. Returns
  1281. =======
  1282. radius : number or instance of Basic
  1283. See Also
  1284. ========
  1285. sympy.geometry.line.Segment.length, sympy.geometry.ellipse.Circle.radius
  1286. Examples
  1287. ========
  1288. >>> from sympy import Symbol
  1289. >>> from sympy import RegularPolygon, Point
  1290. >>> radius = Symbol('r')
  1291. >>> rp = RegularPolygon(Point(0, 0), radius, 4)
  1292. >>> rp.radius
  1293. r
  1294. """
  1295. return self._radius
  1296. @property
  1297. def circumradius(self):
  1298. """
  1299. Alias for radius.
  1300. Examples
  1301. ========
  1302. >>> from sympy import Symbol
  1303. >>> from sympy import RegularPolygon, Point
  1304. >>> radius = Symbol('r')
  1305. >>> rp = RegularPolygon(Point(0, 0), radius, 4)
  1306. >>> rp.circumradius
  1307. r
  1308. """
  1309. return self.radius
  1310. @property
  1311. def rotation(self):
  1312. """CCW angle by which the RegularPolygon is rotated
  1313. Returns
  1314. =======
  1315. rotation : number or instance of Basic
  1316. Examples
  1317. ========
  1318. >>> from sympy import pi
  1319. >>> from sympy.abc import a
  1320. >>> from sympy import RegularPolygon, Point
  1321. >>> RegularPolygon(Point(0, 0), 3, 4, pi/4).rotation
  1322. pi/4
  1323. Numerical rotation angles are made canonical:
  1324. >>> RegularPolygon(Point(0, 0), 3, 4, a).rotation
  1325. a
  1326. >>> RegularPolygon(Point(0, 0), 3, 4, pi).rotation
  1327. 0
  1328. """
  1329. return self._rot
  1330. @property
  1331. def apothem(self):
  1332. """The inradius of the RegularPolygon.
  1333. The apothem/inradius is the radius of the inscribed circle.
  1334. Returns
  1335. =======
  1336. apothem : number or instance of Basic
  1337. See Also
  1338. ========
  1339. sympy.geometry.line.Segment.length, sympy.geometry.ellipse.Circle.radius
  1340. Examples
  1341. ========
  1342. >>> from sympy import Symbol
  1343. >>> from sympy import RegularPolygon, Point
  1344. >>> radius = Symbol('r')
  1345. >>> rp = RegularPolygon(Point(0, 0), radius, 4)
  1346. >>> rp.apothem
  1347. sqrt(2)*r/2
  1348. """
  1349. return self.radius * cos(S.Pi/self._n)
  1350. @property
  1351. def inradius(self):
  1352. """
  1353. Alias for apothem.
  1354. Examples
  1355. ========
  1356. >>> from sympy import Symbol
  1357. >>> from sympy import RegularPolygon, Point
  1358. >>> radius = Symbol('r')
  1359. >>> rp = RegularPolygon(Point(0, 0), radius, 4)
  1360. >>> rp.inradius
  1361. sqrt(2)*r/2
  1362. """
  1363. return self.apothem
  1364. @property
  1365. def interior_angle(self):
  1366. """Measure of the interior angles.
  1367. Returns
  1368. =======
  1369. interior_angle : number
  1370. See Also
  1371. ========
  1372. sympy.geometry.line.LinearEntity.angle_between
  1373. Examples
  1374. ========
  1375. >>> from sympy import RegularPolygon, Point
  1376. >>> rp = RegularPolygon(Point(0, 0), 4, 8)
  1377. >>> rp.interior_angle
  1378. 3*pi/4
  1379. """
  1380. return (self._n - 2)*S.Pi/self._n
  1381. @property
  1382. def exterior_angle(self):
  1383. """Measure of the exterior angles.
  1384. Returns
  1385. =======
  1386. exterior_angle : number
  1387. See Also
  1388. ========
  1389. sympy.geometry.line.LinearEntity.angle_between
  1390. Examples
  1391. ========
  1392. >>> from sympy import RegularPolygon, Point
  1393. >>> rp = RegularPolygon(Point(0, 0), 4, 8)
  1394. >>> rp.exterior_angle
  1395. pi/4
  1396. """
  1397. return 2*S.Pi/self._n
  1398. @property
  1399. def circumcircle(self):
  1400. """The circumcircle of the RegularPolygon.
  1401. Returns
  1402. =======
  1403. circumcircle : Circle
  1404. See Also
  1405. ========
  1406. circumcenter, sympy.geometry.ellipse.Circle
  1407. Examples
  1408. ========
  1409. >>> from sympy import RegularPolygon, Point
  1410. >>> rp = RegularPolygon(Point(0, 0), 4, 8)
  1411. >>> rp.circumcircle
  1412. Circle(Point2D(0, 0), 4)
  1413. """
  1414. return Circle(self.center, self.radius)
  1415. @property
  1416. def incircle(self):
  1417. """The incircle of the RegularPolygon.
  1418. Returns
  1419. =======
  1420. incircle : Circle
  1421. See Also
  1422. ========
  1423. inradius, sympy.geometry.ellipse.Circle
  1424. Examples
  1425. ========
  1426. >>> from sympy import RegularPolygon, Point
  1427. >>> rp = RegularPolygon(Point(0, 0), 4, 7)
  1428. >>> rp.incircle
  1429. Circle(Point2D(0, 0), 4*cos(pi/7))
  1430. """
  1431. return Circle(self.center, self.apothem)
  1432. @property
  1433. def angles(self):
  1434. """
  1435. Returns a dictionary with keys, the vertices of the Polygon,
  1436. and values, the interior angle at each vertex.
  1437. Examples
  1438. ========
  1439. >>> from sympy import RegularPolygon, Point
  1440. >>> r = RegularPolygon(Point(0, 0), 5, 3)
  1441. >>> r.angles
  1442. {Point2D(-5/2, -5*sqrt(3)/2): pi/3,
  1443. Point2D(-5/2, 5*sqrt(3)/2): pi/3,
  1444. Point2D(5, 0): pi/3}
  1445. """
  1446. ret = {}
  1447. ang = self.interior_angle
  1448. for v in self.vertices:
  1449. ret[v] = ang
  1450. return ret
  1451. def encloses_point(self, p):
  1452. """
  1453. Return True if p is enclosed by (is inside of) self.
  1454. Notes
  1455. =====
  1456. Being on the border of self is considered False.
  1457. The general Polygon.encloses_point method is called only if
  1458. a point is not within or beyond the incircle or circumcircle,
  1459. respectively.
  1460. Parameters
  1461. ==========
  1462. p : Point
  1463. Returns
  1464. =======
  1465. encloses_point : True, False or None
  1466. See Also
  1467. ========
  1468. sympy.geometry.ellipse.Ellipse.encloses_point
  1469. Examples
  1470. ========
  1471. >>> from sympy import RegularPolygon, S, Point, Symbol
  1472. >>> p = RegularPolygon((0, 0), 3, 4)
  1473. >>> p.encloses_point(Point(0, 0))
  1474. True
  1475. >>> r, R = p.inradius, p.circumradius
  1476. >>> p.encloses_point(Point((r + R)/2, 0))
  1477. True
  1478. >>> p.encloses_point(Point(R/2, R/2 + (R - r)/10))
  1479. False
  1480. >>> t = Symbol('t', real=True)
  1481. >>> p.encloses_point(p.arbitrary_point().subs(t, S.Half))
  1482. False
  1483. >>> p.encloses_point(Point(5, 5))
  1484. False
  1485. """
  1486. c = self.center
  1487. d = Segment(c, p).length
  1488. if d >= self.radius:
  1489. return False
  1490. elif d < self.inradius:
  1491. return True
  1492. else:
  1493. # now enumerate the RegularPolygon like a general polygon.
  1494. return Polygon.encloses_point(self, p)
  1495. def spin(self, angle):
  1496. """Increment *in place* the virtual Polygon's rotation by ccw angle.
  1497. See also: rotate method which moves the center.
  1498. >>> from sympy import Polygon, Point, pi
  1499. >>> r = Polygon(Point(0,0), 1, n=3)
  1500. >>> r.vertices[0]
  1501. Point2D(1, 0)
  1502. >>> r.spin(pi/6)
  1503. >>> r.vertices[0]
  1504. Point2D(sqrt(3)/2, 1/2)
  1505. See Also
  1506. ========
  1507. rotation
  1508. rotate : Creates a copy of the RegularPolygon rotated about a Point
  1509. """
  1510. self._rot += angle
  1511. def rotate(self, angle, pt=None):
  1512. """Override GeometryEntity.rotate to first rotate the RegularPolygon
  1513. about its center.
  1514. >>> from sympy import Point, RegularPolygon, pi
  1515. >>> t = RegularPolygon(Point(1, 0), 1, 3)
  1516. >>> t.vertices[0] # vertex on x-axis
  1517. Point2D(2, 0)
  1518. >>> t.rotate(pi/2).vertices[0] # vertex on y axis now
  1519. Point2D(0, 2)
  1520. See Also
  1521. ========
  1522. rotation
  1523. spin : Rotates a RegularPolygon in place
  1524. """
  1525. r = type(self)(*self.args) # need a copy or else changes are in-place
  1526. r._rot += angle
  1527. return GeometryEntity.rotate(r, angle, pt)
  1528. def scale(self, x=1, y=1, pt=None):
  1529. """Override GeometryEntity.scale since it is the radius that must be
  1530. scaled (if x == y) or else a new Polygon must be returned.
  1531. >>> from sympy import RegularPolygon
  1532. Symmetric scaling returns a RegularPolygon:
  1533. >>> RegularPolygon((0, 0), 1, 4).scale(2, 2)
  1534. RegularPolygon(Point2D(0, 0), 2, 4, 0)
  1535. Asymmetric scaling returns a kite as a Polygon:
  1536. >>> RegularPolygon((0, 0), 1, 4).scale(2, 1)
  1537. Polygon(Point2D(2, 0), Point2D(0, 1), Point2D(-2, 0), Point2D(0, -1))
  1538. """
  1539. if pt:
  1540. pt = Point(pt, dim=2)
  1541. return self.translate(*(-pt).args).scale(x, y).translate(*pt.args)
  1542. if x != y:
  1543. return Polygon(*self.vertices).scale(x, y)
  1544. c, r, n, rot = self.args
  1545. r *= x
  1546. return self.func(c, r, n, rot)
  1547. def reflect(self, line):
  1548. """Override GeometryEntity.reflect since this is not made of only
  1549. points.
  1550. Examples
  1551. ========
  1552. >>> from sympy import RegularPolygon, Line
  1553. >>> RegularPolygon((0, 0), 1, 4).reflect(Line((0, 1), slope=-2))
  1554. RegularPolygon(Point2D(4/5, 2/5), -1, 4, atan(4/3))
  1555. """
  1556. c, r, n, rot = self.args
  1557. v = self.vertices[0]
  1558. d = v - c
  1559. cc = c.reflect(line)
  1560. vv = v.reflect(line)
  1561. dd = vv - cc
  1562. # calculate rotation about the new center
  1563. # which will align the vertices
  1564. l1 = Ray((0, 0), dd)
  1565. l2 = Ray((0, 0), d)
  1566. ang = l1.closing_angle(l2)
  1567. rot += ang
  1568. # change sign of radius as point traversal is reversed
  1569. return self.func(cc, -r, n, rot)
  1570. @property
  1571. def vertices(self):
  1572. """The vertices of the RegularPolygon.
  1573. Returns
  1574. =======
  1575. vertices : list
  1576. Each vertex is a Point.
  1577. See Also
  1578. ========
  1579. sympy.geometry.point.Point
  1580. Examples
  1581. ========
  1582. >>> from sympy import RegularPolygon, Point
  1583. >>> rp = RegularPolygon(Point(0, 0), 5, 4)
  1584. >>> rp.vertices
  1585. [Point2D(5, 0), Point2D(0, 5), Point2D(-5, 0), Point2D(0, -5)]
  1586. """
  1587. c = self._center
  1588. r = abs(self._radius)
  1589. rot = self._rot
  1590. v = 2*S.Pi/self._n
  1591. return [Point(c.x + r*cos(k*v + rot), c.y + r*sin(k*v + rot))
  1592. for k in range(self._n)]
  1593. def __eq__(self, o):
  1594. if not isinstance(o, Polygon):
  1595. return False
  1596. elif not isinstance(o, RegularPolygon):
  1597. return Polygon.__eq__(o, self)
  1598. return self.args == o.args
  1599. def __hash__(self):
  1600. return super().__hash__()
  1601. class Triangle(Polygon):
  1602. """
  1603. A polygon with three vertices and three sides.
  1604. Parameters
  1605. ==========
  1606. points : sequence of Points
  1607. keyword: asa, sas, or sss to specify sides/angles of the triangle
  1608. Attributes
  1609. ==========
  1610. vertices
  1611. altitudes
  1612. orthocenter
  1613. circumcenter
  1614. circumradius
  1615. circumcircle
  1616. inradius
  1617. incircle
  1618. exradii
  1619. medians
  1620. medial
  1621. nine_point_circle
  1622. Raises
  1623. ======
  1624. GeometryError
  1625. If the number of vertices is not equal to three, or one of the vertices
  1626. is not a Point, or a valid keyword is not given.
  1627. See Also
  1628. ========
  1629. sympy.geometry.point.Point, Polygon
  1630. Examples
  1631. ========
  1632. >>> from sympy import Triangle, Point
  1633. >>> Triangle(Point(0, 0), Point(4, 0), Point(4, 3))
  1634. Triangle(Point2D(0, 0), Point2D(4, 0), Point2D(4, 3))
  1635. Keywords sss, sas, or asa can be used to give the desired
  1636. side lengths (in order) and interior angles (in degrees) that
  1637. define the triangle:
  1638. >>> Triangle(sss=(3, 4, 5))
  1639. Triangle(Point2D(0, 0), Point2D(3, 0), Point2D(3, 4))
  1640. >>> Triangle(asa=(30, 1, 30))
  1641. Triangle(Point2D(0, 0), Point2D(1, 0), Point2D(1/2, sqrt(3)/6))
  1642. >>> Triangle(sas=(1, 45, 2))
  1643. Triangle(Point2D(0, 0), Point2D(2, 0), Point2D(sqrt(2)/2, sqrt(2)/2))
  1644. """
  1645. def __new__(cls, *args, **kwargs):
  1646. if len(args) != 3:
  1647. if 'sss' in kwargs:
  1648. return _sss(*[simplify(a) for a in kwargs['sss']])
  1649. if 'asa' in kwargs:
  1650. return _asa(*[simplify(a) for a in kwargs['asa']])
  1651. if 'sas' in kwargs:
  1652. return _sas(*[simplify(a) for a in kwargs['sas']])
  1653. msg = "Triangle instantiates with three points or a valid keyword."
  1654. raise GeometryError(msg)
  1655. vertices = [Point(a, dim=2, **kwargs) for a in args]
  1656. # remove consecutive duplicates
  1657. nodup = []
  1658. for p in vertices:
  1659. if nodup and p == nodup[-1]:
  1660. continue
  1661. nodup.append(p)
  1662. if len(nodup) > 1 and nodup[-1] == nodup[0]:
  1663. nodup.pop() # last point was same as first
  1664. # remove collinear points
  1665. i = -3
  1666. while i < len(nodup) - 3 and len(nodup) > 2:
  1667. a, b, c = sorted(
  1668. [nodup[i], nodup[i + 1], nodup[i + 2]], key=default_sort_key)
  1669. if Point.is_collinear(a, b, c):
  1670. nodup[i] = a
  1671. nodup[i + 1] = None
  1672. nodup.pop(i + 1)
  1673. i += 1
  1674. vertices = list(filter(lambda x: x is not None, nodup))
  1675. if len(vertices) == 3:
  1676. return GeometryEntity.__new__(cls, *vertices, **kwargs)
  1677. elif len(vertices) == 2:
  1678. return Segment(*vertices, **kwargs)
  1679. else:
  1680. return Point(*vertices, **kwargs)
  1681. @property
  1682. def vertices(self):
  1683. """The triangle's vertices
  1684. Returns
  1685. =======
  1686. vertices : tuple
  1687. Each element in the tuple is a Point
  1688. See Also
  1689. ========
  1690. sympy.geometry.point.Point
  1691. Examples
  1692. ========
  1693. >>> from sympy import Triangle, Point
  1694. >>> t = Triangle(Point(0, 0), Point(4, 0), Point(4, 3))
  1695. >>> t.vertices
  1696. (Point2D(0, 0), Point2D(4, 0), Point2D(4, 3))
  1697. """
  1698. return self.args
  1699. def is_similar(t1, t2):
  1700. """Is another triangle similar to this one.
  1701. Two triangles are similar if one can be uniformly scaled to the other.
  1702. Parameters
  1703. ==========
  1704. other: Triangle
  1705. Returns
  1706. =======
  1707. is_similar : boolean
  1708. See Also
  1709. ========
  1710. sympy.geometry.entity.GeometryEntity.is_similar
  1711. Examples
  1712. ========
  1713. >>> from sympy import Triangle, Point
  1714. >>> t1 = Triangle(Point(0, 0), Point(4, 0), Point(4, 3))
  1715. >>> t2 = Triangle(Point(0, 0), Point(-4, 0), Point(-4, -3))
  1716. >>> t1.is_similar(t2)
  1717. True
  1718. >>> t2 = Triangle(Point(0, 0), Point(-4, 0), Point(-4, -4))
  1719. >>> t1.is_similar(t2)
  1720. False
  1721. """
  1722. if not isinstance(t2, Polygon):
  1723. return False
  1724. s1_1, s1_2, s1_3 = [side.length for side in t1.sides]
  1725. s2 = [side.length for side in t2.sides]
  1726. def _are_similar(u1, u2, u3, v1, v2, v3):
  1727. e1 = simplify(u1/v1)
  1728. e2 = simplify(u2/v2)
  1729. e3 = simplify(u3/v3)
  1730. return bool(e1 == e2) and bool(e2 == e3)
  1731. # There's only 6 permutations, so write them out
  1732. return _are_similar(s1_1, s1_2, s1_3, *s2) or \
  1733. _are_similar(s1_1, s1_3, s1_2, *s2) or \
  1734. _are_similar(s1_2, s1_1, s1_3, *s2) or \
  1735. _are_similar(s1_2, s1_3, s1_1, *s2) or \
  1736. _are_similar(s1_3, s1_1, s1_2, *s2) or \
  1737. _are_similar(s1_3, s1_2, s1_1, *s2)
  1738. def is_equilateral(self):
  1739. """Are all the sides the same length?
  1740. Returns
  1741. =======
  1742. is_equilateral : boolean
  1743. See Also
  1744. ========
  1745. sympy.geometry.entity.GeometryEntity.is_similar, RegularPolygon
  1746. is_isosceles, is_right, is_scalene
  1747. Examples
  1748. ========
  1749. >>> from sympy import Triangle, Point
  1750. >>> t1 = Triangle(Point(0, 0), Point(4, 0), Point(4, 3))
  1751. >>> t1.is_equilateral()
  1752. False
  1753. >>> from sympy import sqrt
  1754. >>> t2 = Triangle(Point(0, 0), Point(10, 0), Point(5, 5*sqrt(3)))
  1755. >>> t2.is_equilateral()
  1756. True
  1757. """
  1758. return not has_variety(s.length for s in self.sides)
  1759. def is_isosceles(self):
  1760. """Are two or more of the sides the same length?
  1761. Returns
  1762. =======
  1763. is_isosceles : boolean
  1764. See Also
  1765. ========
  1766. is_equilateral, is_right, is_scalene
  1767. Examples
  1768. ========
  1769. >>> from sympy import Triangle, Point
  1770. >>> t1 = Triangle(Point(0, 0), Point(4, 0), Point(2, 4))
  1771. >>> t1.is_isosceles()
  1772. True
  1773. """
  1774. return has_dups(s.length for s in self.sides)
  1775. def is_scalene(self):
  1776. """Are all the sides of the triangle of different lengths?
  1777. Returns
  1778. =======
  1779. is_scalene : boolean
  1780. See Also
  1781. ========
  1782. is_equilateral, is_isosceles, is_right
  1783. Examples
  1784. ========
  1785. >>> from sympy import Triangle, Point
  1786. >>> t1 = Triangle(Point(0, 0), Point(4, 0), Point(1, 4))
  1787. >>> t1.is_scalene()
  1788. True
  1789. """
  1790. return not has_dups(s.length for s in self.sides)
  1791. def is_right(self):
  1792. """Is the triangle right-angled.
  1793. Returns
  1794. =======
  1795. is_right : boolean
  1796. See Also
  1797. ========
  1798. sympy.geometry.line.LinearEntity.is_perpendicular
  1799. is_equilateral, is_isosceles, is_scalene
  1800. Examples
  1801. ========
  1802. >>> from sympy import Triangle, Point
  1803. >>> t1 = Triangle(Point(0, 0), Point(4, 0), Point(4, 3))
  1804. >>> t1.is_right()
  1805. True
  1806. """
  1807. s = self.sides
  1808. return Segment.is_perpendicular(s[0], s[1]) or \
  1809. Segment.is_perpendicular(s[1], s[2]) or \
  1810. Segment.is_perpendicular(s[0], s[2])
  1811. @property
  1812. def altitudes(self):
  1813. """The altitudes of the triangle.
  1814. An altitude of a triangle is a segment through a vertex,
  1815. perpendicular to the opposite side, with length being the
  1816. height of the vertex measured from the line containing the side.
  1817. Returns
  1818. =======
  1819. altitudes : dict
  1820. The dictionary consists of keys which are vertices and values
  1821. which are Segments.
  1822. See Also
  1823. ========
  1824. sympy.geometry.point.Point, sympy.geometry.line.Segment.length
  1825. Examples
  1826. ========
  1827. >>> from sympy import Point, Triangle
  1828. >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1)
  1829. >>> t = Triangle(p1, p2, p3)
  1830. >>> t.altitudes[p1]
  1831. Segment2D(Point2D(0, 0), Point2D(1/2, 1/2))
  1832. """
  1833. s = self.sides
  1834. v = self.vertices
  1835. return {v[0]: s[1].perpendicular_segment(v[0]),
  1836. v[1]: s[2].perpendicular_segment(v[1]),
  1837. v[2]: s[0].perpendicular_segment(v[2])}
  1838. @property
  1839. def orthocenter(self):
  1840. """The orthocenter of the triangle.
  1841. The orthocenter is the intersection of the altitudes of a triangle.
  1842. It may lie inside, outside or on the triangle.
  1843. Returns
  1844. =======
  1845. orthocenter : Point
  1846. See Also
  1847. ========
  1848. sympy.geometry.point.Point
  1849. Examples
  1850. ========
  1851. >>> from sympy import Point, Triangle
  1852. >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1)
  1853. >>> t = Triangle(p1, p2, p3)
  1854. >>> t.orthocenter
  1855. Point2D(0, 0)
  1856. """
  1857. a = self.altitudes
  1858. v = self.vertices
  1859. return Line(a[v[0]]).intersection(Line(a[v[1]]))[0]
  1860. @property
  1861. def circumcenter(self):
  1862. """The circumcenter of the triangle
  1863. The circumcenter is the center of the circumcircle.
  1864. Returns
  1865. =======
  1866. circumcenter : Point
  1867. See Also
  1868. ========
  1869. sympy.geometry.point.Point
  1870. Examples
  1871. ========
  1872. >>> from sympy import Point, Triangle
  1873. >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1)
  1874. >>> t = Triangle(p1, p2, p3)
  1875. >>> t.circumcenter
  1876. Point2D(1/2, 1/2)
  1877. """
  1878. a, b, c = [x.perpendicular_bisector() for x in self.sides]
  1879. return a.intersection(b)[0]
  1880. @property
  1881. def circumradius(self):
  1882. """The radius of the circumcircle of the triangle.
  1883. Returns
  1884. =======
  1885. circumradius : number of Basic instance
  1886. See Also
  1887. ========
  1888. sympy.geometry.ellipse.Circle.radius
  1889. Examples
  1890. ========
  1891. >>> from sympy import Symbol
  1892. >>> from sympy import Point, Triangle
  1893. >>> a = Symbol('a')
  1894. >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, a)
  1895. >>> t = Triangle(p1, p2, p3)
  1896. >>> t.circumradius
  1897. sqrt(a**2/4 + 1/4)
  1898. """
  1899. return Point.distance(self.circumcenter, self.vertices[0])
  1900. @property
  1901. def circumcircle(self):
  1902. """The circle which passes through the three vertices of the triangle.
  1903. Returns
  1904. =======
  1905. circumcircle : Circle
  1906. See Also
  1907. ========
  1908. sympy.geometry.ellipse.Circle
  1909. Examples
  1910. ========
  1911. >>> from sympy import Point, Triangle
  1912. >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1)
  1913. >>> t = Triangle(p1, p2, p3)
  1914. >>> t.circumcircle
  1915. Circle(Point2D(1/2, 1/2), sqrt(2)/2)
  1916. """
  1917. return Circle(self.circumcenter, self.circumradius)
  1918. def bisectors(self):
  1919. """The angle bisectors of the triangle.
  1920. An angle bisector of a triangle is a straight line through a vertex
  1921. which cuts the corresponding angle in half.
  1922. Returns
  1923. =======
  1924. bisectors : dict
  1925. Each key is a vertex (Point) and each value is the corresponding
  1926. bisector (Segment).
  1927. See Also
  1928. ========
  1929. sympy.geometry.point.Point, sympy.geometry.line.Segment
  1930. Examples
  1931. ========
  1932. >>> from sympy import Point, Triangle, Segment
  1933. >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1)
  1934. >>> t = Triangle(p1, p2, p3)
  1935. >>> from sympy import sqrt
  1936. >>> t.bisectors()[p2] == Segment(Point(1, 0), Point(0, sqrt(2) - 1))
  1937. True
  1938. """
  1939. # use lines containing sides so containment check during
  1940. # intersection calculation can be avoided, thus reducing
  1941. # the processing time for calculating the bisectors
  1942. s = [Line(l) for l in self.sides]
  1943. v = self.vertices
  1944. c = self.incenter
  1945. l1 = Segment(v[0], Line(v[0], c).intersection(s[1])[0])
  1946. l2 = Segment(v[1], Line(v[1], c).intersection(s[2])[0])
  1947. l3 = Segment(v[2], Line(v[2], c).intersection(s[0])[0])
  1948. return {v[0]: l1, v[1]: l2, v[2]: l3}
  1949. @property
  1950. def incenter(self):
  1951. """The center of the incircle.
  1952. The incircle is the circle which lies inside the triangle and touches
  1953. all three sides.
  1954. Returns
  1955. =======
  1956. incenter : Point
  1957. See Also
  1958. ========
  1959. incircle, sympy.geometry.point.Point
  1960. Examples
  1961. ========
  1962. >>> from sympy import Point, Triangle
  1963. >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1)
  1964. >>> t = Triangle(p1, p2, p3)
  1965. >>> t.incenter
  1966. Point2D(1 - sqrt(2)/2, 1 - sqrt(2)/2)
  1967. """
  1968. s = self.sides
  1969. l = Matrix([s[i].length for i in [1, 2, 0]])
  1970. p = sum(l)
  1971. v = self.vertices
  1972. x = simplify(l.dot(Matrix([vi.x for vi in v]))/p)
  1973. y = simplify(l.dot(Matrix([vi.y for vi in v]))/p)
  1974. return Point(x, y)
  1975. @property
  1976. def inradius(self):
  1977. """The radius of the incircle.
  1978. Returns
  1979. =======
  1980. inradius : number of Basic instance
  1981. See Also
  1982. ========
  1983. incircle, sympy.geometry.ellipse.Circle.radius
  1984. Examples
  1985. ========
  1986. >>> from sympy import Point, Triangle
  1987. >>> p1, p2, p3 = Point(0, 0), Point(4, 0), Point(0, 3)
  1988. >>> t = Triangle(p1, p2, p3)
  1989. >>> t.inradius
  1990. 1
  1991. """
  1992. return simplify(2 * self.area / self.perimeter)
  1993. @property
  1994. def incircle(self):
  1995. """The incircle of the triangle.
  1996. The incircle is the circle which lies inside the triangle and touches
  1997. all three sides.
  1998. Returns
  1999. =======
  2000. incircle : Circle
  2001. See Also
  2002. ========
  2003. sympy.geometry.ellipse.Circle
  2004. Examples
  2005. ========
  2006. >>> from sympy import Point, Triangle
  2007. >>> p1, p2, p3 = Point(0, 0), Point(2, 0), Point(0, 2)
  2008. >>> t = Triangle(p1, p2, p3)
  2009. >>> t.incircle
  2010. Circle(Point2D(2 - sqrt(2), 2 - sqrt(2)), 2 - sqrt(2))
  2011. """
  2012. return Circle(self.incenter, self.inradius)
  2013. @property
  2014. def exradii(self):
  2015. """The radius of excircles of a triangle.
  2016. An excircle of the triangle is a circle lying outside the triangle,
  2017. tangent to one of its sides and tangent to the extensions of the
  2018. other two.
  2019. Returns
  2020. =======
  2021. exradii : dict
  2022. See Also
  2023. ========
  2024. sympy.geometry.polygon.Triangle.inradius
  2025. Examples
  2026. ========
  2027. The exradius touches the side of the triangle to which it is keyed, e.g.
  2028. the exradius touching side 2 is:
  2029. >>> from sympy import Point, Triangle
  2030. >>> p1, p2, p3 = Point(0, 0), Point(6, 0), Point(0, 2)
  2031. >>> t = Triangle(p1, p2, p3)
  2032. >>> t.exradii[t.sides[2]]
  2033. -2 + sqrt(10)
  2034. References
  2035. ==========
  2036. .. [1] https://mathworld.wolfram.com/Exradius.html
  2037. .. [2] https://mathworld.wolfram.com/Excircles.html
  2038. """
  2039. side = self.sides
  2040. a = side[0].length
  2041. b = side[1].length
  2042. c = side[2].length
  2043. s = (a+b+c)/2
  2044. area = self.area
  2045. exradii = {self.sides[0]: simplify(area/(s-a)),
  2046. self.sides[1]: simplify(area/(s-b)),
  2047. self.sides[2]: simplify(area/(s-c))}
  2048. return exradii
  2049. @property
  2050. def excenters(self):
  2051. """Excenters of the triangle.
  2052. An excenter is the center of a circle that is tangent to a side of the
  2053. triangle and the extensions of the other two sides.
  2054. Returns
  2055. =======
  2056. excenters : dict
  2057. Examples
  2058. ========
  2059. The excenters are keyed to the side of the triangle to which their corresponding
  2060. excircle is tangent: The center is keyed, e.g. the excenter of a circle touching
  2061. side 0 is:
  2062. >>> from sympy import Point, Triangle
  2063. >>> p1, p2, p3 = Point(0, 0), Point(6, 0), Point(0, 2)
  2064. >>> t = Triangle(p1, p2, p3)
  2065. >>> t.excenters[t.sides[0]]
  2066. Point2D(12*sqrt(10), 2/3 + sqrt(10)/3)
  2067. See Also
  2068. ========
  2069. sympy.geometry.polygon.Triangle.exradii
  2070. References
  2071. ==========
  2072. .. [1] https://mathworld.wolfram.com/Excircles.html
  2073. """
  2074. s = self.sides
  2075. v = self.vertices
  2076. a = s[0].length
  2077. b = s[1].length
  2078. c = s[2].length
  2079. x = [v[0].x, v[1].x, v[2].x]
  2080. y = [v[0].y, v[1].y, v[2].y]
  2081. exc_coords = {
  2082. "x1": simplify(-a*x[0]+b*x[1]+c*x[2]/(-a+b+c)),
  2083. "x2": simplify(a*x[0]-b*x[1]+c*x[2]/(a-b+c)),
  2084. "x3": simplify(a*x[0]+b*x[1]-c*x[2]/(a+b-c)),
  2085. "y1": simplify(-a*y[0]+b*y[1]+c*y[2]/(-a+b+c)),
  2086. "y2": simplify(a*y[0]-b*y[1]+c*y[2]/(a-b+c)),
  2087. "y3": simplify(a*y[0]+b*y[1]-c*y[2]/(a+b-c))
  2088. }
  2089. excenters = {
  2090. s[0]: Point(exc_coords["x1"], exc_coords["y1"]),
  2091. s[1]: Point(exc_coords["x2"], exc_coords["y2"]),
  2092. s[2]: Point(exc_coords["x3"], exc_coords["y3"])
  2093. }
  2094. return excenters
  2095. @property
  2096. def medians(self):
  2097. """The medians of the triangle.
  2098. A median of a triangle is a straight line through a vertex and the
  2099. midpoint of the opposite side, and divides the triangle into two
  2100. equal areas.
  2101. Returns
  2102. =======
  2103. medians : dict
  2104. Each key is a vertex (Point) and each value is the median (Segment)
  2105. at that point.
  2106. See Also
  2107. ========
  2108. sympy.geometry.point.Point.midpoint, sympy.geometry.line.Segment.midpoint
  2109. Examples
  2110. ========
  2111. >>> from sympy import Point, Triangle
  2112. >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1)
  2113. >>> t = Triangle(p1, p2, p3)
  2114. >>> t.medians[p1]
  2115. Segment2D(Point2D(0, 0), Point2D(1/2, 1/2))
  2116. """
  2117. s = self.sides
  2118. v = self.vertices
  2119. return {v[0]: Segment(v[0], s[1].midpoint),
  2120. v[1]: Segment(v[1], s[2].midpoint),
  2121. v[2]: Segment(v[2], s[0].midpoint)}
  2122. @property
  2123. def medial(self):
  2124. """The medial triangle of the triangle.
  2125. The triangle which is formed from the midpoints of the three sides.
  2126. Returns
  2127. =======
  2128. medial : Triangle
  2129. See Also
  2130. ========
  2131. sympy.geometry.line.Segment.midpoint
  2132. Examples
  2133. ========
  2134. >>> from sympy import Point, Triangle
  2135. >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1)
  2136. >>> t = Triangle(p1, p2, p3)
  2137. >>> t.medial
  2138. Triangle(Point2D(1/2, 0), Point2D(1/2, 1/2), Point2D(0, 1/2))
  2139. """
  2140. s = self.sides
  2141. return Triangle(s[0].midpoint, s[1].midpoint, s[2].midpoint)
  2142. @property
  2143. def nine_point_circle(self):
  2144. """The nine-point circle of the triangle.
  2145. Nine-point circle is the circumcircle of the medial triangle, which
  2146. passes through the feet of altitudes and the middle points of segments
  2147. connecting the vertices and the orthocenter.
  2148. Returns
  2149. =======
  2150. nine_point_circle : Circle
  2151. See also
  2152. ========
  2153. sympy.geometry.line.Segment.midpoint
  2154. sympy.geometry.polygon.Triangle.medial
  2155. sympy.geometry.polygon.Triangle.orthocenter
  2156. Examples
  2157. ========
  2158. >>> from sympy import Point, Triangle
  2159. >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1)
  2160. >>> t = Triangle(p1, p2, p3)
  2161. >>> t.nine_point_circle
  2162. Circle(Point2D(1/4, 1/4), sqrt(2)/4)
  2163. """
  2164. return Circle(*self.medial.vertices)
  2165. @property
  2166. def eulerline(self):
  2167. """The Euler line of the triangle.
  2168. The line which passes through circumcenter, centroid and orthocenter.
  2169. Returns
  2170. =======
  2171. eulerline : Line (or Point for equilateral triangles in which case all
  2172. centers coincide)
  2173. Examples
  2174. ========
  2175. >>> from sympy import Point, Triangle
  2176. >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1)
  2177. >>> t = Triangle(p1, p2, p3)
  2178. >>> t.eulerline
  2179. Line2D(Point2D(0, 0), Point2D(1/2, 1/2))
  2180. """
  2181. if self.is_equilateral():
  2182. return self.orthocenter
  2183. return Line(self.orthocenter, self.circumcenter)
  2184. def rad(d):
  2185. """Return the radian value for the given degrees (pi = 180 degrees)."""
  2186. return d*pi/180
  2187. def deg(r):
  2188. """Return the degree value for the given radians (pi = 180 degrees)."""
  2189. return r/pi*180
  2190. def _slope(d):
  2191. rv = tan(rad(d))
  2192. return rv
  2193. def _asa(d1, l, d2):
  2194. """Return triangle having side with length l on the x-axis."""
  2195. xy = Line((0, 0), slope=_slope(d1)).intersection(
  2196. Line((l, 0), slope=_slope(180 - d2)))[0]
  2197. return Triangle((0, 0), (l, 0), xy)
  2198. def _sss(l1, l2, l3):
  2199. """Return triangle having side of length l1 on the x-axis."""
  2200. c1 = Circle((0, 0), l3)
  2201. c2 = Circle((l1, 0), l2)
  2202. inter = [a for a in c1.intersection(c2) if a.y.is_nonnegative]
  2203. if not inter:
  2204. return None
  2205. pt = inter[0]
  2206. return Triangle((0, 0), (l1, 0), pt)
  2207. def _sas(l1, d, l2):
  2208. """Return triangle having side with length l2 on the x-axis."""
  2209. p1 = Point(0, 0)
  2210. p2 = Point(l2, 0)
  2211. p3 = Point(cos(rad(d))*l1, sin(rad(d))*l1)
  2212. return Triangle(p1, p2, p3)