diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/.nojekyll @@ -0,0 +1 @@ + diff --git a/README.md b/README.md new file mode 100644 index 00000000..ebfb3665 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# documentation diff --git a/docs/.buildinfo b/docs/.buildinfo new file mode 100644 index 00000000..51413209 --- /dev/null +++ b/docs/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 1831c489016e44a64004140d03e7dca8 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/.doctrees/environment.pickle b/docs/.doctrees/environment.pickle new file mode 100644 index 00000000..93bfb147 Binary files /dev/null and b/docs/.doctrees/environment.pickle differ diff --git a/docs/.doctrees/index.doctree b/docs/.doctrees/index.doctree new file mode 100644 index 00000000..f53e227e Binary files /dev/null and b/docs/.doctrees/index.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.boolean.doctree b/docs/.doctrees/ladybug_geometry.boolean.doctree new file mode 100644 index 00000000..9d4e4296 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.boolean.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.bounding.doctree b/docs/.doctrees/ladybug_geometry.bounding.doctree new file mode 100644 index 00000000..3a419e97 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.bounding.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.dictutil.doctree b/docs/.doctrees/ladybug_geometry.dictutil.doctree new file mode 100644 index 00000000..e8afa610 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.dictutil.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.doctree b/docs/.doctrees/ladybug_geometry.doctree new file mode 100644 index 00000000..a5123a8d Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry2d.arc.doctree b/docs/.doctrees/ladybug_geometry.geometry2d.arc.doctree new file mode 100644 index 00000000..33dc3889 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry2d.arc.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry2d.doctree b/docs/.doctrees/ladybug_geometry.geometry2d.doctree new file mode 100644 index 00000000..78ed2a14 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry2d.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry2d.line.doctree b/docs/.doctrees/ladybug_geometry.geometry2d.line.doctree new file mode 100644 index 00000000..77d9b30d Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry2d.line.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry2d.mesh.doctree b/docs/.doctrees/ladybug_geometry.geometry2d.mesh.doctree new file mode 100644 index 00000000..7b250ea4 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry2d.mesh.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry2d.pointvector.doctree b/docs/.doctrees/ladybug_geometry.geometry2d.pointvector.doctree new file mode 100644 index 00000000..df51ba37 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry2d.pointvector.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry2d.polygon.doctree b/docs/.doctrees/ladybug_geometry.geometry2d.polygon.doctree new file mode 100644 index 00000000..0db8ddc2 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry2d.polygon.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry2d.polyline.doctree b/docs/.doctrees/ladybug_geometry.geometry2d.polyline.doctree new file mode 100644 index 00000000..8b8ba7e8 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry2d.polyline.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry2d.ray.doctree b/docs/.doctrees/ladybug_geometry.geometry2d.ray.doctree new file mode 100644 index 00000000..2629e5f9 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry2d.ray.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.arc.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.arc.doctree new file mode 100644 index 00000000..ccc54108 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.arc.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.cone.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.cone.doctree new file mode 100644 index 00000000..9d4ed70b Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.cone.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.cylinder.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.cylinder.doctree new file mode 100644 index 00000000..6803bb08 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.cylinder.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.doctree new file mode 100644 index 00000000..2f0080ad Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.face.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.face.doctree new file mode 100644 index 00000000..715602d9 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.face.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.line.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.line.doctree new file mode 100644 index 00000000..493e8336 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.line.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.mesh.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.mesh.doctree new file mode 100644 index 00000000..2a831066 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.mesh.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.plane.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.plane.doctree new file mode 100644 index 00000000..fb5e5ed7 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.plane.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.pointvector.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.pointvector.doctree new file mode 100644 index 00000000..c3b3a146 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.pointvector.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.polyface.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.polyface.doctree new file mode 100644 index 00000000..c8fc8861 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.polyface.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.polyline.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.polyline.doctree new file mode 100644 index 00000000..87a8c176 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.polyline.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.ray.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.ray.doctree new file mode 100644 index 00000000..1551853c Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.ray.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.geometry3d.sphere.doctree b/docs/.doctrees/ladybug_geometry.geometry3d.sphere.doctree new file mode 100644 index 00000000..98f60809 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.geometry3d.sphere.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.interop.doctree b/docs/.doctrees/ladybug_geometry.interop.doctree new file mode 100644 index 00000000..f72ba49d Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.interop.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.interop.obj.doctree b/docs/.doctrees/ladybug_geometry.interop.obj.doctree new file mode 100644 index 00000000..4c796b62 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.interop.obj.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.interop.stl.doctree b/docs/.doctrees/ladybug_geometry.interop.stl.doctree new file mode 100644 index 00000000..3035bae8 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.interop.stl.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.intersection2d.doctree b/docs/.doctrees/ladybug_geometry.intersection2d.doctree new file mode 100644 index 00000000..eebc8fac Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.intersection2d.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.intersection3d.doctree b/docs/.doctrees/ladybug_geometry.intersection3d.doctree new file mode 100644 index 00000000..d1f966aa Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.intersection3d.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.network.doctree b/docs/.doctrees/ladybug_geometry.network.doctree new file mode 100644 index 00000000..905c440a Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.network.doctree differ diff --git a/docs/.doctrees/ladybug_geometry.triangulation.doctree b/docs/.doctrees/ladybug_geometry.triangulation.doctree new file mode 100644 index 00000000..3143ff32 Binary files /dev/null and b/docs/.doctrees/ladybug_geometry.triangulation.doctree differ diff --git a/docs/.doctrees/modules.doctree b/docs/.doctrees/modules.doctree new file mode 100644 index 00000000..a1bc1397 Binary files /dev/null and b/docs/.doctrees/modules.doctree differ diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..ebfb3665 --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +# documentation diff --git a/docs/_modules/index.html b/docs/_modules/index.html new file mode 100644 index 00000000..7d40dcfc --- /dev/null +++ b/docs/_modules/index.html @@ -0,0 +1,1182 @@ + + + + + + + Overview: module code — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/boolean.html b/docs/_modules/ladybug_geometry/boolean.html new file mode 100644 index 00000000..b1ba7883 --- /dev/null +++ b/docs/_modules/ladybug_geometry/boolean.html @@ -0,0 +1,2404 @@ + + + + + + + ladybug_geometry.boolean — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.boolean

+# coding=utf-8
+"""Module for boolean operations on 2D polygons (union, intersection, difference, xor).
+
+The functions here are derived from the pypolybool python library available at
+https://github.com/KaivnD/pypolybool
+
+The pypolybool library is, itself, a pure Python port of the polybooljs JavaScript
+library maintained by Sean Mconnelly available at https://github.com/velipso/polybooljs
+
+Full documentation of the method is available at
+https://sean.cm/a/polygon-clipping-pt2
+
+Based somewhat on the F. Martinez (2013) algorithm.
+
+Francisco Martínez, Carlos Ogayar, Juan R. Jiménez, Antonio J. Rueda (2013),
+"A simple algorithm for Boolean operations on polygons",
+Advances in Engineering Software, Volume 64, Pages 11-19, ISSN 0965-9978,
+https://doi.org/10.1016/j.advengsoft.2013.04.004.
+"""
+from __future__ import division
+
+
+"""____________OBJECTS FOR INPUT/OUTPUT FROM BOOLEAN OPERATIONS____________"""
+
+
+
+[docs] +class BooleanPoint: + """2D Point class used in polygon boolean operations. + + Args: + x: Float for X coordinate. + y: Float for Y coordinate + """ + + def __init__(self, x, y): + self.x = x + self.y = y + +
+[docs] + def is_equivalent(self, other_pt, tol): + """Check if this point is equivalent ot another within tolerance.""" + if not isinstance(other_pt, BooleanPoint): + return False + return abs(self.x - other_pt.x) < tol and abs(self.y - other_pt.y) < tol
+ + +
+[docs] + @staticmethod + def collinear(pt1, pt2, pt3, tolerance): + """Get a boolean for whether 3 points are colinear.""" + dx1 = pt1.x - pt2.x + dy1 = pt1.y - pt2.y + dx2 = pt2.x - pt3.x + dy2 = pt2.y - pt3.y + return abs(dx1 * dy2 - dx2 * dy1) < tolerance
+ + +
+[docs] + @staticmethod + def compare(pt1, pt2, tolerance): + """Get an integer for the relationship between two points. + + Zero indicates equal. Positive 1 is to the right. Negative 1 is to the left. + """ + if abs(pt1.x - pt2.x) < tolerance: + return 0 if abs(pt1.y - pt2.y) < tolerance else -1 if pt1.y < pt2.y else 1 + return -1 if pt1.x < pt2.x else 1
+ + +
+[docs] + @staticmethod + def point_above_or_on_line(point, left, right, tolerance): + """Get a boolean for whether a point is above or on a line. + + Args: + point: BooleanPoint to be evaluated. + left: BooleanPoint for the left of the line segment. + right: BooleanPoint for the right of the line segment + """ + return (right.x - left.x) * (point.y - left.y) - (right.y - left.y) * ( + point.x - left.x + ) >= -tolerance
+ + +
+[docs] + @staticmethod + def between(point, left, right, tolerance): + """Get a boolean for whether a point is between two points. + + Args: + point: BooleanPoint to be evaluated. + left: BooleanPoint for the left. + right: BooleanPoint for the right. + """ + dPyLy = point.y - left.y + dRxLx = right.x - left.x + dPxLx = point.x - left.x + dRyLy = right.y - left.y + + dot = dPxLx * dRxLx + dPyLy * dRyLy + if dot < tolerance: + return False + + sqlen = dRxLx * dRxLx + dRyLy * dRyLy + if dot - sqlen > -tolerance: + return False + + return True
+ + + @staticmethod + def _lines_intersect(a0, a1, b0, b1, tolerance): + """Get an _IntersectionPoint object for the intersection of two line segments. + + Args: + a0: BooleanPoint for the first point of the first line segment. + a1: BooleanPoint for the second point of the first line segment. + b0: BooleanPoint for the first point of the second line segment. + b1: BooleanPoint for the second point of the second line segment. + """ + adx = a1.x - a0.x + ady = a1.y - a0.y + bdx = b1.x - b0.x + bdy = b1.y - b0.y + + axb = adx * bdy - ady * bdx + + if abs(axb) < tolerance: + return None + + dx = a0.x - b0.x + dy = a0.y - b0.y + + a = (bdx * dy - bdy * dx) / axb + b = (adx * dy - ady * dx) / axb + + return _IntersectionPoint( + BooleanPoint.__calc_along_using_value(a, tolerance), + BooleanPoint.__calc_along_using_value(b, tolerance), + BooleanPoint(a0.x + a * adx, a0.y + a * ady), + ) + + @staticmethod + def __calc_along_using_value(value, tolerance): + if value <= -tolerance: + return -2 + elif value < tolerance: + return -1 + elif value - 1 <= -tolerance: + return 0 + elif value - 1 < tolerance: + return 1 + else: + return 2 + + def __repr__(self): + return "{},{}".format(self.x, self.y) + + def __str__(self): + return "{},{}".format(self.x, self.y)
+ + + +
+[docs] +class BooleanPolygon: + """Polygon class used in polygon boolean operations. + + Args: + regions: A list of lists of BooleanPoints representing the 2D points defining + the regions of the Polygon. The first sub-list is typically the + boundary of the polygon and each successive list represents a hole + within the boundary. It is also permissable for the holes + to lie outside the first polygon, in which case the shape is + interpreted as a MultiPolygon. As an alternative to BooleanPoints, + tuples of two float values are also permissable in which case the + values represent the X and Y coordinates of each vertex. + is_inverted: A boolean for whether the Polygon is inverted or not. For + polygons input to the boolean methods, this value should always be + False. (Default: False) + """ + + def __init__(self, regions, is_inverted=False): + _regions = [] + for region in regions: + tmp = [] + for pt in region: + if isinstance(pt, BooleanPoint): + tmp.append(pt) + elif isinstance(pt, tuple): + x, y = pt + tmp.append(BooleanPoint(x, y)) + _regions.append(tmp) + + self.regions = _regions + self.is_inverted = is_inverted
+ + + +"""____________PRIMARY COMPUTATION CLASSES AND METHODS____________""" + + +class _Fill: + def __init__(self, below=None, above=None): + self.below = below + self.above = above + + def __repr__(self): + return "{},{}".format(self.above, self.below) + + def __str__(self): + return "{},{}".format(self.above, self.below) + + +class _Segment: + def __init__(self, start, end, myfill=None, otherfill=None): + self.start = start + self.end = end + self.myfill = myfill + self.otherfill = otherfill + + def __repr__(self): + return "S: {}, E: {}".format(self.start, self.end) + + def __str__(self): + return "S: {}, E: {}".format(self.start, self.end) + + +class _PolySegments: + def __init__(self, segments=None, is_inverted=False): + self.segments = segments + self.is_inverted = is_inverted + + +class _CombinedPolySegments: + def __init__(self, combined=None, is_inverted1=False, is_inverted2=False): + self.combined = combined + self.is_inverted1 = is_inverted1 + self.is_inverted2 = is_inverted2 + + +class _Matcher: + def __init__(self, index, matchesHead, matchesPt1): + self.index = index + self.matchesHead = matchesHead + self.matchesPt1 = matchesPt1 + + +class _IntersectionPoint: + def __init__(self, alongA, alongB, pt): + self.alongA = alongA + self.alongB = alongB + self.pt = pt + + +class _Node: + def __init__( + self, isRoot=False, isStart=False, pt=None, seg=None, primary=False, + next=None, previous=None, other=None, ev=None, status=None, remove=None, + ): + self.status = status + self.other = other + self.ev = ev + self.previous = previous + self.next = next + self.isRoot = isRoot + self.remove = remove + self.isStart = isStart + self.pt = pt + self.seg = seg + self.primary = primary + + +class _Transition: + def __init__(self, after, before, insert): + self.after = after + self.before = before + self.insert = insert + + +class _LinkedList: + def __init__(self): + self.__root = _Node(isRoot=True) + + def exists(self, node): + if node is None or node is self.__root: + return False + return True + + def isEmpty(self): + return self.__root.next is None + + def getHead(self): + return self.__root.next + + def insertBefore(self, node, check): + last = self.__root + here = self.__root.next + + while here is not None: + if check(here): + node.previous = here.previous + node.next = here + here.previous.next = node + here.previous = node + return + last = here + here = here.next + last.next = node + node.previous = last + node.next = None + + def findTransition(self, check): + previous = self.__root + here = self.__root.next + + while here is not None: + if check(here): + break + previous = here + here = here.next + + def insert_func(node): + node.previous = previous + node.next = here + previous.next = node + if here is not None: + here.previous = node + return node + + return _Transition( + before=(None if previous is self.__root else previous), + after=here, + insert=insert_func, + ) + + @staticmethod + def node(data): + data.previous = None + data.next = None + + def remove_func(): + data.previous.next = data.next + if data.next is not None: + data.next.previous = data.previous + data.previous = None + data.next = None + + data.remove = remove_func + return data + + +class _Intersecter: + """Primary intersection class.""" + + def __init__(self, selfIntersection, tol): + self.selfIntersection = selfIntersection + self.tol = tol + self.__eventRoot = _LinkedList() + + def newsegment(self, start, end): + return _Segment(start=start, end=end, myfill=_Fill()) + + def segmentCopy(self, start, end, seg): + return _Segment( + start=start, end=end, myfill=_Fill(seg.myfill.below, seg.myfill.above) + ) + + def __eventCompare(self, p1IsStart, p11, p12, p2IsStart, p21, p22): + comp = BooleanPoint.compare(p11, p21, self.tol) + if comp != 0: + return comp + + if p12.is_equivalent(p22, self.tol): + return 0 + + if p1IsStart != p2IsStart: + return 1 if p1IsStart else -1 + + return ( + 1 + if BooleanPoint.point_above_or_on_line( + p12, p21 if p2IsStart else p22, p22 if p2IsStart else p21, self.tol + ) + else -1 + ) + + def __eventAdd(self, ev, otherPt): + def check_func(here): + comp = self.__eventCompare( + ev.isStart, ev.pt, otherPt, here.isStart, here.pt, here.other.pt + ) + return comp < 0 + + self.__eventRoot.insertBefore(ev, check_func) + + def __eventAddSegmentStart(self, segment, primary): + evStart = _LinkedList.node( + _Node( + isStart=True, + pt=segment.start, + seg=segment, + primary=primary, + ) + ) + self.__eventAdd(evStart, segment.end) + return evStart + + def __eventAddSegmentEnd(self, evStart, segment, primary): + evEnd = _LinkedList.node( + _Node( + isStart=False, + pt=segment.end, + seg=segment, + primary=primary, + other=evStart, + ) + ) + evStart.other = evEnd + self.__eventAdd(evEnd, evStart.pt) + + def eventAddSegment(self, segment, primary): + evStart = self.__eventAddSegmentStart(segment, primary) + self.__eventAddSegmentEnd(evStart, segment, primary) + return evStart + + def __eventUpdateEnd(self, ev, end): + ev.other.remove() + ev.seg.end = end + ev.other.pt = end + self.__eventAdd(ev.other, ev.pt) + + def __eventDivide(self, ev, pt): + ns = self.segmentCopy(pt, ev.seg.end, ev.seg) + self.__eventUpdateEnd(ev, pt) + return self.eventAddSegment(ns, ev.primary) + + def __statusCompare(self, ev1, ev2): + a1 = ev1.seg.start + a2 = ev1.seg.end + b1 = ev2.seg.start + b2 = ev2.seg.end + + if BooleanPoint.collinear(a1, b1, b2, self.tol): + if BooleanPoint.collinear(a2, b1, b2, self.tol): + return 1 + return 1 if BooleanPoint.point_above_or_on_line(a2, b1, b2, self.tol) else -1 + return 1 if BooleanPoint.point_above_or_on_line(a1, b1, b2, self.tol) else -1 + + def __statusFindSurrounding(self, statusRoot, ev): + def check_func(here): + return self.__statusCompare(ev, here.ev) > 0 + + return statusRoot.findTransition(check_func) + + def __checkIntersection(self, ev1, ev2): + seg1 = ev1.seg + seg2 = ev2.seg + a1 = seg1.start + a2 = seg1.end + b1 = seg2.start + b2 = seg2.end + + i = BooleanPoint._lines_intersect(a1, a2, b1, b2, self.tol) + if i is None: + if not BooleanPoint.collinear(a1, a2, b1, self.tol): + return None + if a1.is_equivalent(b2, self.tol) or a2.is_equivalent(b1, self.tol): + return None + a1EquB1 = a1.is_equivalent(b1, self.tol) + a2EquB2 = a2.is_equivalent(b2, self.tol) + if a1EquB1 and a2EquB2: + return ev2 + + a1Between = not a1EquB1 and BooleanPoint.between(a1, b1, b2, self.tol) + a2Between = not a2EquB2 and BooleanPoint.between(a2, b1, b2, self.tol) + + if a1EquB1: + if a2Between: + self.__eventDivide(ev2, a2) + else: + self.__eventDivide(ev1, b2) + + return ev2 + elif a1Between: + if not a2EquB2: + if a2Between: + self.__eventDivide(ev2, a2) + else: + self.__eventDivide(ev1, b2) + self.__eventDivide(ev2, a1) + else: + if i.alongA == 0: + if i.alongB == -1: + self.__eventDivide(ev1, b1) + elif i.alongB == 0: + self.__eventDivide(ev1, i.pt) + elif i.alongB == 1: + self.__eventDivide(ev1, b2) + if i.alongB == 0: + if i.alongA == -1: + self.__eventDivide(ev2, a1) + elif i.alongA == 0: + self.__eventDivide(ev2, i.pt) + elif i.alongA == 1: + self.__eventDivide(ev2, a2) + return None + + def __checkBothIntersections(self, above, ev, below): + if above is not None: + eve = self.__checkIntersection(ev, above) + if eve is not None: + return eve + if below is not None: + return self.__checkIntersection(ev, below) + + return None + + def calculate(self, primaryPolyInverted, secondaryPolyInverted): + statusRoot = _LinkedList() + segments = [] + + cnt = 0 + + while not self.__eventRoot.isEmpty(): + cnt += 1 + ev = self.__eventRoot.getHead() + if ev.isStart: + surrounding = self.__statusFindSurrounding(statusRoot, ev) + above = ( + surrounding.before.ev if surrounding.before is not None else None + ) + below = surrounding.after.ev if surrounding.after is not None else None + + eve = self.__checkBothIntersections(above, ev, below) + if eve is not None: + if self.selfIntersection: + toggle = False + if ev.seg.myfill.below is None: + toggle = True + else: + toggle = ev.seg.myfill.above != ev.seg.myfill.below + + if toggle: + eve.seg.myfill.above = not eve.seg.myfill.above + else: + eve.seg.otherfill = ev.seg.myfill + ev.other.remove() + ev.remove() + + if self.__eventRoot.getHead() is not ev: + continue + + if self.selfIntersection: + toggle = False + if ev.seg.myfill.below is None: + toggle = True + else: + toggle = ev.seg.myfill.above != ev.seg.myfill.below + + if below is None: + ev.seg.myfill.below = primaryPolyInverted + else: + ev.seg.myfill.below = below.seg.myfill.above + + if toggle: + ev.seg.myfill.above = not ev.seg.myfill.below + else: + ev.seg.myfill.above = ev.seg.myfill.below + else: + if ev.seg.otherfill is None: + inside = False + if below is None: + inside = ( + secondaryPolyInverted + if ev.primary + else primaryPolyInverted + ) + else: + if ev.primary == below.primary: + inside = below.seg.otherfill.above + else: + inside = below.seg.myfill.above + ev.seg.otherfill = _Fill(inside, inside) + ev.other.status = surrounding.insert(_LinkedList.node(_Node(ev=ev))) + else: + st = ev.status + if st is None: + raise Exception( + 'PolyBool: Zero-length segment detected; ' + 'your tolerance is probably too small or too large' + ) + if statusRoot.exists(st.previous) and statusRoot.exists(st.next): + self.__checkIntersection(st.previous.ev, st.next.ev) + st.remove() + + if not ev.primary: + s = ev.seg.myfill + ev.seg.myfill = ev.seg.otherfill + ev.seg.otherfill = s + segments.append(ev.seg) + self.__eventRoot.getHead().remove() + return segments + + +class _RegionIntersecter(_Intersecter): + def __init__(self, tol): + _Intersecter.__init__(self, True, tol) + + def addRegion(self, region): + pt2 = region[-1] + for i in range(len(region)): + pt1 = pt2 + pt2 = region[i] + forward = BooleanPoint.compare(pt1, pt2, self.tol) + + if forward == 0: + continue + + seg = self.newsegment( + pt1 if forward < 0 else pt2, pt2 if forward < 0 else pt1 + ) + + self.eventAddSegment(seg, True) + + def calculate(self, inverted): + return _Intersecter.calculate(self, inverted, False) + + +class _SegmentIntersecter(_Intersecter): + def __init__(self, tol): + _Intersecter.__init__(self, False, tol) + + def calculate( + self, segments1, is_inverted1, segments2, is_inverted2 + ): + for seg in segments1: + self.eventAddSegment(self.segmentCopy(seg.start, seg.end, seg), True) + + for seg in segments2: + self.eventAddSegment(self.segmentCopy(seg.start, seg.end, seg), False) + + return _Intersecter.calculate(self, is_inverted1, is_inverted2) + + +class _SegmentChainerMatcher: + def __init__(self): + self.firstMatch = _Matcher(0, False, False) + self.secondMatch = _Matcher(0, False, False) + + self.nextMatch = self.firstMatch + + def setMatch(self, index, matchesHead, matchesPt1): + self.nextMatch.index = index + self.nextMatch.matchesHead = matchesHead + self.nextMatch.matchesPt1 = matchesPt1 + if self.nextMatch is self.firstMatch: + self.nextMatch = self.secondMatch + return False + self.nextMatch = None + return True + + +def _list_shift(list): + list.pop(0) + + +def _list_pop(list): + list.pop() + + +def _list_splice(list, index, count): + del list[index: index + count] + + +def _list_unshift(list, element): + list.insert(0, element) + + +def _segmentChainer(segments, tol): + regions = [] + chains = [] + + for seg in segments: + pt1 = seg.start + pt2 = seg.end + if pt1.is_equivalent(pt2, tol): + continue + + scm = _SegmentChainerMatcher() + + for i in range(len(chains)): + chain = chains[i] + head = chain[0] + tail = chain[-1] + + if head.is_equivalent(pt1, tol): + if scm.setMatch(i, True, True): + break + elif head.is_equivalent(pt2, tol): + if scm.setMatch(i, True, False): + break + elif tail.is_equivalent(pt1, tol): + if scm.setMatch(i, False, True): + break + elif tail.is_equivalent(pt2, tol): + if scm.setMatch(i, False, False): + break + + if scm.nextMatch is scm.firstMatch: + chains.append([pt1, pt2]) + continue + + if scm.nextMatch is scm.secondMatch: + index = scm.firstMatch.index + pt = pt2 if scm.firstMatch.matchesPt1 else pt1 + addToHead = scm.firstMatch.matchesHead + + chain = chains[index] + grow = chain[0] if addToHead else chain[-1] + grow2 = chain[1] if addToHead else chain[-2] + oppo = chain[-1] if addToHead else chain[0] + oppo2 = chain[-2] if addToHead else chain[1] + + if BooleanPoint.collinear(grow2, grow, pt, tol): + if addToHead: + _list_shift(chain) + else: + _list_pop(chain) + grow = grow2 + if oppo.is_equivalent(pt, tol): + _list_splice(chains, index, 1) + if BooleanPoint.collinear(oppo2, oppo, grow, tol): + if addToHead: + _list_pop(chain) + else: + _list_shift(chain) + regions.append(chain) + continue + if addToHead: + _list_unshift(chain, pt) + else: + chain.append(pt) + continue + + def reverseChain(index): + chains[index].reverse() + + def appendChain(index1, index2): + chain1 = chains[index1] + chain2 = chains[index2] + tail = chain1[-1] + tail2 = chain1[-2] + head = chain2[0] + head2 = chain2[1] + + if BooleanPoint.collinear(tail2, tail, head, tol): + _list_pop(chain1) + tail = tail2 + if BooleanPoint.collinear(tail, head, head2, tol): + _list_shift(chain2) + + chains[index1] = chain1 + chain2 + _list_splice(chains, index2, 1) + + f = scm.firstMatch.index + s = scm.secondMatch.index + + reverseF = len(chains[f]) < len(chains[s]) + if scm.firstMatch.matchesHead: + if scm.secondMatch.matchesHead: + if reverseF: + reverseChain(f) + appendChain(f, s) + else: + reverseChain(s) + appendChain(s, f) + else: + appendChain(s, f) + else: + if scm.secondMatch.matchesHead: + appendChain(f, s) + else: + if reverseF: + reverseChain(f) + appendChain(s, f) + else: + reverseChain(s) + appendChain(f, s) + + return regions + + +def __select(segments, selection): + result = [] + for seg in segments: + index = ( + (8 if seg.myfill.above else 0) + + (4 if seg.myfill.below else 0) + + (2 if seg.otherfill is not None and seg.otherfill.above else 0) + + (1 if seg.otherfill is not None and seg.otherfill.below else 0) + ) + + if selection[index] != 0: + result.append( + _Segment( + start=seg.start, + end=seg.end, + myfill=_Fill(selection[index] == 2, above=selection[index] == 1), + ) + ) + return result + + +"""____________CORE INTERFACE FOR MANAGING INTERSECTIONS____________""" + + +def _segments(poly, tol): + """Get the intersected PolySegments of a BooleanPolygon. + + Args: + poly: A BooleanPolygon for which PolySegments will be computed. + tol: The intersection tolerance. + """ + i = _RegionIntersecter(tol) + for region in poly.regions: + i.addRegion(region) + return _PolySegments(i.calculate(poly.is_inverted), poly.is_inverted) + + +def _combine(segments1, segments2, tol): + """Combine intersected PolySegments into a CombinedPolySegments object. + + Args: + segments1: The first PolySegments object to be combined. + segments2: The second PolySegments to be combined. + tol: The intersection tolerance. + """ + i = _SegmentIntersecter(tol) + return _CombinedPolySegments( + i.calculate( + segments1.segments, + segments1.is_inverted, + segments2.segments, + segments2.is_inverted, + ), + segments1.is_inverted, + segments2.is_inverted, + ) + + +def _select_union(polyseg): + """Select the union from the PolySegments. + + above1 below1 above2 below2 Keep? Value + 0 0 0 0 => no 0 + 0 0 0 1 => yes filled below 2 + 0 0 1 0 => yes filled above 1 + 0 0 1 1 => no 0 + 0 1 0 0 => yes filled below 2 + 0 1 0 1 => yes filled below 2 + 0 1 1 0 => no 0 + 0 1 1 1 => no 0 + 1 0 0 0 => yes filled above 1 + 1 0 0 1 => no 0 + 1 0 1 0 => yes filled above 1 + 1 0 1 1 => no 0 + 1 1 0 0 => no 0 + 1 1 0 1 => no 0 + 1 1 1 0 => no 0 + 1 1 1 1 => no 0 + """ + return _PolySegments( + segments=__select( + # fmt:off + polyseg.combined, [ + 0, 2, 1, 0, + 2, 2, 0, 0, + 1, 0, 1, 0, + 0, 0, 0, 0, + ] + # fmt:on + ), + is_inverted=(polyseg.is_inverted1 or polyseg.is_inverted2), + ) + + +def _select_intersect(polyseg): + """Select the intersection from the PolySegments. + + above1 below1 above2 below2 Keep? Value + 0 0 0 0 => no 0 + 0 0 0 1 => no 0 + 0 0 1 0 => no 0 + 0 0 1 1 => no 0 + 0 1 0 0 => no 0 + 0 1 0 1 => yes filled below 2 + 0 1 1 0 => no 0 + 0 1 1 1 => yes filled below 2 + 1 0 0 0 => no 0 + 1 0 0 1 => no 0 + 1 0 1 0 => yes filled above 1 + 1 0 1 1 => yes filled above 1 + 1 1 0 0 => no 0 + 1 1 0 1 => yes filled below 2 + 1 1 1 0 => yes filled above 1 + 1 1 1 1 => no 0 + """ + return _PolySegments( + segments=__select( + # fmt:off + polyseg.combined, [ + 0, 0, 0, 0, + 0, 2, 0, 2, + 0, 0, 1, 1, + 0, 2, 1, 0 + ] + # fmt:on + ), + is_inverted=(polyseg.is_inverted1 and polyseg.is_inverted2), + ) + + +def _select_difference(polyseg): + """Select the difference from the PolySegments. + + above1 below1 above2 below2 Keep? Value + 0 0 0 0 => no 0 + 0 0 0 1 => no 0 + 0 0 1 0 => no 0 + 0 0 1 1 => no 0 + 0 1 0 0 => yes filled below 2 + 0 1 0 1 => no 0 + 0 1 1 0 => yes filled below 2 + 0 1 1 1 => no 0 + 1 0 0 0 => yes filled above 1 + 1 0 0 1 => yes filled above 1 + 1 0 1 0 => no 0 + 1 0 1 1 => no 0 + 1 1 0 0 => no 0 + 1 1 0 1 => yes filled above 1 + 1 1 1 0 => yes filled below 2 + 1 1 1 1 => no 0 + """ + return _PolySegments( + segments=__select( + # fmt:off + polyseg.combined, [ + 0, 0, 0, 0, + 2, 0, 2, 0, + 1, 1, 0, 0, + 0, 1, 2, 0 + ] + # fmt:on + ), + is_inverted=(polyseg.is_inverted1 and not polyseg.is_inverted2), + ) + + +def _select_difference_rev(polyseg): + """Select the reversed difference from the PolySegments. + + above1 below1 above2 below2 Keep? Value + 0 0 0 0 => no 0 + 0 0 0 1 => yes filled below 2 + 0 0 1 0 => yes filled above 1 + 0 0 1 1 => no 0 + 0 1 0 0 => no 0 + 0 1 0 1 => no 0 + 0 1 1 0 => yes filled above 1 + 0 1 1 1 => yes filled above 1 + 1 0 0 0 => no 0 + 1 0 0 1 => yes filled below 2 + 1 0 1 0 => no 0 + 1 0 1 1 => yes filled below 2 + 1 1 0 0 => no 0 + 1 1 0 1 => no 0 + 1 1 1 0 => no 0 + 1 1 1 1 => no 0 + """ + return _PolySegments( + segments=__select( + # fmt:off + polyseg.combined, [ + 0, 2, 1, 0, + 0, 0, 1, 1, + 0, 2, 0, 2, + 0, 0, 0, 0 + ] + # fmt:on + ), + is_inverted=(not polyseg.is_inverted1 and polyseg.is_inverted2), + ) + + +def _select_xor(polyseg): + """Select the exclusive disjunction from the PolySegments. + + above1 below1 above2 below2 Keep? Value + 0 0 0 0 => no 0 + 0 0 0 1 => yes filled below 2 + 0 0 1 0 => yes filled above 1 + 0 0 1 1 => no 0 + 0 1 0 0 => yes filled below 2 + 0 1 0 1 => no 0 + 0 1 1 0 => no 0 + 0 1 1 1 => yes filled above 1 + 1 0 0 0 => yes filled above 1 + 1 0 0 1 => no 0 + 1 0 1 0 => no 0 + 1 0 1 1 => yes filled below 2 + 1 1 0 0 => no 0 + 1 1 0 1 => yes filled above 1 + 1 1 1 0 => yes filled below 2 + 1 1 1 1 => no 0 + """ + return _PolySegments( + segments=__select( + # fmt:off + polyseg.combined, [ + 0, 2, 1, 0, + 2, 0, 0, 1, + 1, 0, 0, 2, + 0, 1, 2, 0 + ] + # fmt:on + ), + is_inverted=(polyseg.is_inverted1 != polyseg.is_inverted2), + ) + + +def _polygon(segments, tol): + return BooleanPolygon(_segmentChainer(segments.segments, tol), segments.is_inverted) + + +def __operate(poly1, poly2, selector, tol): + firstPolygonRegions = _segments(poly1, tol) + secondPolygonRegions = _segments(poly2, tol) + combinedSegments = _combine(firstPolygonRegions, secondPolygonRegions, tol) + seg = selector(combinedSegments) + return _polygon(seg, tol) + + +"""____________PUBLIC FUNCTIONS FOR BOOLEAN OPERATIONS____________""" + + +
+[docs] +def union_all(polygons, tolerance): + """Get a BooleanPolygon for the union of multiple polygons. + + Using this method is more computationally efficient than calling the union() + method multiple times as this method will only compute the intersection of + the segments once. + + Args: + polygons: An array of BooleanPolygons for which the union will be computed. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A BooleanPolygon for the union across all of the input polygons. + """ + seg1 = _segments(polygons[0], tolerance) + for i in range(1, len(polygons)): + seg2 = _segments(polygons[i], tolerance) + comb = _combine(seg1, seg2, tolerance) + seg1 = _select_union(comb) + return _polygon(seg1, tolerance)
+ + + +
+[docs] +def intersect_all(polygons, tolerance): + """Get a BooleanPolygon for the intersection of multiple polygons. + + Using this method is more computationally efficient than calling the intersect() + method multiple times as this method will only compute the intersection of + the segments once. + + Args: + polygons: An array of BooleanPolygons for which the intersection will + be computed. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A BooleanPolygon for the intersection across all of the input polygons. + """ + seg1 = _segments(polygons[0], tolerance) + for i in range(1, len(polygons)): + seg2 = _segments(polygons[i], tolerance) + comb = _combine(seg1, seg2, tolerance) + seg1 = _select_intersect(comb) + return _polygon(seg1, tolerance)
+ + + +
+[docs] +def split(poly1, poly2, tolerance): + """Split two BooleanPolygons with one another to get the intersection and difference. + + Using this method is more computationally efficient than calling the intersect() + and difference() methods individually as this method will only compute the + intersection of the segments once. + + Args: + poly1: A BooleanPolygon for the first polygon that will be split with + the second polygon. + poly2: A BooleanPolygon for the second polygon that will be split with + the first polygon. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A tuple with three elements + + - intersection: A BooleanPolygon for the intersection of the two + input polygons. + + - poly1_difference: A BooleanPolygon for the portion of poly1 that does + not overlap with poly2. When combined with the intersection, this + makes a split version of poly1. + + - poly2_difference: A BooleanPolygon for the portion of poly2 that does + not overlap with poly1. When combined with the intersection, this + makes a split version of poly2. + """ + first_regions = _segments(poly1, tolerance) + second_regions = _segments(poly2, tolerance) + comb = _combine(first_regions, second_regions, tolerance) + intersection = _polygon(_select_intersect(comb), tolerance) + poly1_difference = _polygon(_select_difference(comb), tolerance) + poly2_difference = _polygon(_select_difference_rev(comb), tolerance) + return intersection, poly1_difference, poly2_difference
+ + + +
+[docs] +def union(poly1, poly2, tolerance): + """Get a BooleanPolygon for the union of two polygons. + + Note that the result will not differentiate hole polygons from boundary polygons. + + Args: + poly1: A BooleanPolygon for the first polygon for which the union will + be computed. + poly2: A BooleanPolygon for the second polygon for which the union will + be computed. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A BooleanPolygon for the union of the two polygons. + """ + return __operate(poly1, poly2, _select_union, tolerance)
+ + + +
+[docs] +def intersect(poly1, poly2, tolerance): + """Get a BooleanPolygon for the intersection of two polygons. + + Args: + poly1: A BooleanPolygon for the first polygon for which the intersection + will be computed. + poly2: A BooleanPolygon for the second polygon for which the intersection + will be computed. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A BooleanPolygon for the intersection of the two polygons. + """ + return __operate(poly1, poly2, _select_intersect, tolerance)
+ + + +
+[docs] +def difference(poly1, poly2, tolerance): + """Get a BooleanPolygon for the subtraction of poly2 from poly1. + + Args: + poly1: A BooleanPolygon for the the polygon that will be subtracted from. + poly2: A BooleanPolygon for the polygon to subtract with. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A BooleanPolygon for the difference of poly1 - poly2. + """ + return __operate(poly1, poly2, _select_difference, tolerance)
+ + + +
+[docs] +def difference_reversed(poly1, poly2, tolerance): + """Get a BooleanPolygon for the subtraction of poly1 from poly2. + + Args: + poly1: A BooleanPolygon for the polygon to subtract with. + poly2: A BooleanPolygon for the the polygon that will be subtracted from. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A BooleanPolygon for the difference of poly2 - poly1. + """ + return __operate(poly1, poly2, _select_difference_rev, tolerance)
+ + + +
+[docs] +def xor(poly1, poly2, tolerance): + """Get a BooleanPolygon for the exclusive disjunction of two Polygons. + + Note that this method is prone to merging holes that may exist in the + result into the boundary to create a single list of joined vertices, + which may not always be desirable. In this case, it may be desirable + to do two separate difference calculations instead or use the split method. + + Also note that, when the result includes separate polygons for holes, + it will not differentiate hole polygons from boundary polygons. + + Args: + poly1: A BooleanPolygon for the first polygon for which the exclusive + disjunction will be computed. + poly2: A BooleanPolygon for the second polygon for which the exclusive + disjunction will be computed. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A BooleanPolygon for the exclusive disjunction of the two polygons. + """ + return __operate(poly1, poly2, _select_xor, tolerance)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/bounding.html b/docs/_modules/ladybug_geometry/bounding.html new file mode 100644 index 00000000..7b2a8f44 --- /dev/null +++ b/docs/_modules/ladybug_geometry/bounding.html @@ -0,0 +1,1384 @@ + + + + + + + ladybug_geometry.bounding — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.bounding

+# coding=utf-8
+"""Utility functions for computing bounding boxes and extents around geometry."""
+from __future__ import division
+
+from ladybug_geometry.geometry2d.pointvector import Point2D
+from ladybug_geometry.geometry3d.pointvector import Point3D
+
+
+
+[docs] +def bounding_domain_x(geometries): + """Get minimum and maximum X coordinates of multiple geometries. + + Args: + geometries: An array of any ladybug_geometry objects for which the extents + of the X domain will be computed. Note that all objects must have + a min and max property. + + Returns: + A tuple with the min and the max X coordinates around the geometry. + """ + min_x, max_x = geometries[0].min.x, geometries[0].max.x + for geom in geometries[1:]: + if geom.min.x < min_x: + min_x = geom.min.x + if geom.max.x > max_x: + max_x = geom.max.x + return min_x, max_x
+ + + +
+[docs] +def bounding_domain_y(geometries): + """Get minimum and maximum Y coordinates of multiple geometries. + + Args: + geometries: An array of any ladybug_geometry objects for which the extents + of the Y domain will be computed. Note that all objects must have + a min and max property. + + Returns: + A tuple with the min and the max Y coordinates around the geometry. + """ + min_y, max_y = geometries[0].min.y, geometries[0].max.y + for geom in geometries[1:]: + if geom.min.y < min_y: + min_y = geom.min.y + if geom.max.y > max_y: + max_y = geom.max.y + return min_y, max_y
+ + + +
+[docs] +def bounding_domain_z(geometries): + """Get minimum and maximum Z coordinates of multiple geometries. + + Args: + geometries: An array of any 3D ladybug_geometry objects for which the extents + of the Z domain will be computed. Note that all objects must have + a min and max property and they cannot be 2D objects. + + Returns: + A tuple with the min and the max Z coordinates around the geometry. + """ + min_z, max_z = geometries[0].min.z, geometries[0].max.z + for geom in geometries: + if geom.max.z > max_z: + max_z = geom.max.z + if geom.min.z < min_z: + min_z = geom.min.z + return min_z, max_z
+ + + +
+[docs] +def bounding_domain_z_2d_safe(geometries): + """Get minimum and maximum Z coordinates in a manner that is safe for 2D geometries. + + Args: + geometries: An array of any ladybug_geometry objects for which the extents + of the Z domain will be computed. Any 2D objects within this list will + be assumed to have a Z-value of zero. + + Returns: + A tuple with the min and the max Z coordinates around the geometry. + """ + try: + min_z, max_z = geometries[0].min.z, geometries[0].max.z + except AttributeError: + min_z, max_z = 0, 0 + for geom in geometries: + try: + if geom.max.z > max_z: + max_z = geom.max.z + if geom.min.z < min_z: + min_z = geom.min.z + except AttributeError: + if 0 > max_z: + max_z = 0 + if 0 < min_z: + min_z = 0 + return min_z, max_z
+ + + +def _orient_geometry(geometries, axis_angle, center): + """Orient both 2D and 3D geometry to a given axis angle and center point. + + This is used by the methods that compute bounding rectangles. + """ + new_geometries = [] + for geom in geometries: + try: # assume that it is a 2D geometry object + new_geometries.append(geom.rotate(-axis_angle, center)) + except TypeError: # it's a 3D geometry object + new_geometries.append(geom.rotate_xy(-axis_angle, center)) + return new_geometries + + +
+[docs] +def bounding_rectangle(geometries, axis_angle=0): + """Get the min and max of an oriented bounding rectangle around 2D or 3D geometry. + + Args: + geometries: An array of 2D or 3D geometry objects. Note that all objects + must have a min and max property. + axis_angle: The counter-clockwise rotation angle in radians in the XY plane + to represent the orientation of the bounding rectangle extents. (Default: 0). + + Returns: + A tuple with two Point2D objects representing the min point and max point + of the bounding rectangle respectively. + """ + if axis_angle != 0: # rotate geometry to the bounding box + cpt = geometries[0].vertices[0] + geometries = _orient_geometry(geometries, axis_angle, cpt) + xx = bounding_domain_x(geometries) + yy = bounding_domain_y(geometries) + min_pt = Point2D(xx[0], yy[0]) + max_pt = Point2D(xx[1], yy[1]) + if axis_angle != 0: # rotate the points back + cpt = Point2D(cpt.x, cpt.y) # cast Point3D to Point2D + min_pt = min_pt.rotate(axis_angle, cpt) + max_pt = max_pt.rotate(axis_angle, cpt) + return min_pt, max_pt
+ + + +
+[docs] +def bounding_rectangle_extents(geometries, axis_angle=0): + """Get the width and length of an oriented bounding rectangle around 2D or 3D geometry. + + Args: + geometries: An array of 2D or 3D geometry objects. Note that all objects + must have a min and max property. + axis_angle: The counter-clockwise rotation angle in radians in the XY plane + to represent the orientation of the bounding rectangle extents. (Default: 0). + + Returns: + A tuple with 2 values corresponding to the width and length of the bounding + rectangle. + """ + if axis_angle != 0: + cpt = geometries[0].vertices[0] + geometries = _orient_geometry(geometries, axis_angle, cpt) + xx = bounding_domain_x(geometries) + yy = bounding_domain_y(geometries) + return xx[1] - xx[0], yy[1] - yy[0]
+ + + +
+[docs] +def bounding_box(geometries, axis_angle=0): + """Get the min and max of an oriented bounding box around 3D geometry. + + Args: + geometries: An array of 3D geometry objects. Note that all objects must + have a min and max property. + axis_angle: The counter-clockwise rotation angle in radians in the XY plane + to represent the orientation of the bounding box extents. (Default: 0). + + Returns: + A tuple with two Point3D objects representing the min point and max point + of the bounding box respectively. + """ + if axis_angle != 0: # rotate geometry to the bounding box + cpt = geometries[0].vertices[0] + geometries = [geom.rotate_xy(-axis_angle, cpt) for geom in geometries] + xx = bounding_domain_x(geometries) + yy = bounding_domain_y(geometries) + zz = bounding_domain_z_2d_safe(geometries) + min_pt = Point3D(xx[0], yy[0], zz[0]) + max_pt = Point3D(xx[1], yy[1], zz[1]) + if axis_angle != 0: # rotate the points back + min_pt = min_pt.rotate_xy(axis_angle, cpt) + max_pt = max_pt.rotate_xy(axis_angle, cpt) + return min_pt, max_pt
+ + + +
+[docs] +def bounding_box_extents(geometries, axis_angle=0): + """Get the width, length and height of an oriented bounding box around 3D geometry. + + Args: + geometries: An array of 3D geometry objects. Note that all objects must + have a min and max property. + axis_angle: The counter-clockwise rotation angle in radians in the XY plane + to represent the orientation of the bounding box extents. (Default: 0). + + Returns: + A tuple with 3 values corresponding to the width, length and height of + the bounding box. + """ + if axis_angle != 0: + cpt = geometries[0].vertices[0] + geometries = [geom.rotate_xy(-axis_angle, cpt) for geom in geometries] + xx = bounding_domain_x(geometries) + yy = bounding_domain_y(geometries) + zz = bounding_domain_z_2d_safe(geometries) + return xx[1] - xx[0], yy[1] - yy[0], zz[1] - zz[0]
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/dictutil.html b/docs/_modules/ladybug_geometry/dictutil.html new file mode 100644 index 00000000..d75b756f --- /dev/null +++ b/docs/_modules/ladybug_geometry/dictutil.html @@ -0,0 +1,1224 @@ + + + + + + + ladybug_geometry.dictutil — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.dictutil

+# coding=utf-8
+"""Utilities to convert any Ladybug Geometry dictionary to Python objects.
+
+Note that importing this module will import almost all modules within the
+Ladybug_geometry library in order to be able to re-serialize almost any
+dictionary produced from the library.
+"""
+from __future__ import division
+
+from ladybug_geometry.geometry2d import Vector2D, Point2D, Ray2D, \
+    LineSegment2D, Arc2D, Polyline2D, Polygon2D, Mesh2D
+from ladybug_geometry.geometry3d import Vector3D, Point3D, Ray3D, LineSegment3D, \
+    Arc3D, Polyline3D, Polyface3D, Mesh3D, Plane, Face3D, Sphere, Cone, Cylinder
+
+
+
+[docs] +def geometry_dict_to_object(ladybug_geom_dict, raise_exception=True): + """ + Args: + ladybug_geom_dict (dict): A dictionary of any Ladybug Geometry object. + raise_exception (bool): Boolean to note whether an exception should be raised + if the object is not identified as a part of ladybug_geometry. + Default: True. + + Returns: + A Python object derived from the input ladybug_geom_dict. + """ + + lbt_types = { + 'Vector2D': Vector2D, + 'Point2D': Point2D, + 'Ray2D': Ray2D, + 'LineSegment2D': LineSegment2D, + 'Arc2D': Arc2D, + 'Polyline2D': Polyline2D, + 'Polygon2D': Polygon2D, + 'Mesh2D': Mesh2D, + 'Vector3D': Vector3D, + 'Point3D': Point3D, + 'Ray3D': Ray3D, + 'LineSegment3D': LineSegment3D, + 'Arc3D': Arc3D, + 'Polyline3D': Polyline3D, + 'Mesh3D': Mesh3D, + 'Plane': Plane, + 'Polyface3D': Polyface3D, + 'Face3D': Face3D, + 'Sphere': Sphere, + 'Cone': Cone, + 'Cylinder': Cylinder + } + + # Get the ladybug_geometry object 'Type' + try: + obj_type = ladybug_geom_dict['type'] + except KeyError: + raise ValueError('Ladybug dictionary lacks required "type" key.') + + # Build a new Ladybug Python Object based on the "Type" + try: + lbt_class = lbt_types[obj_type] + return lbt_class.from_dict(ladybug_geom_dict) + except KeyError: + if raise_exception: + raise ValueError( + '{} is not a recognized ladybug geometry type'.format(obj_type)) + else: + return None
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry2d/arc.html b/docs/_modules/ladybug_geometry/geometry2d/arc.html new file mode 100644 index 00000000..33dae3a1 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry2d/arc.html @@ -0,0 +1,1710 @@ + + + + + + + ladybug_geometry.geometry2d.arc — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry2d.arc

+# coding=utf-8
+"""2D Arc"""
+from __future__ import division
+import math
+
+from .pointvector import Point2D, Vector2D
+from .polyline import Polyline2D
+from ..intersection2d import closest_point2d_on_arc2d, intersect_line2d_arc2d, \
+    intersect_line2d_infinite_arc2d
+
+
+
+[docs] +class Arc2D(object): + """2D arc object. + + Args: + c: A Point2D representing the center of the arc. + r: A number representing the radius of the arc. + a1: A number between 0 and 2 * pi for the start angle of the arc. + Note that the direction of the arc is always counterclockwise. + a2: A number between 0 and 2 * pi for the end angle of the arc. + Note that the direction of the arc is always counterclockwise. + + Properties: + * c + * r + * a1 + * a2 + * p1 + * p2 + * midpoint + * min + * max + * length + * angle + * is_circle + * is_inverted + """ + __slots__ = ( + '_c', '_r', '_a1', '_a2', '_cos_a1', '_sin_a1', '_cos_a2', '_sin_a2', + '_min', '_max') + + def __init__(self, c, r, a1=0, a2=2 * math.pi): + """Initialize Arc2D.""" + assert isinstance(c, Point2D), "Expected Point2D. Got {}.".format(type(c)) + assert r > 0, 'Arc radius must be greater than 0. Got {}.'.format(r) + assert 0 <= a1 <= 2 * math.pi, 'Arc start angle must be between 0 and 2*pi. ' \ + 'Got {}.'.format(a1) + assert 0 <= a2 <= 2 * math.pi, 'Arc start angle must be between 0 and 2*pi. ' \ + 'Got {}.'.format(a2) + self._c = c + self._r = r + self._a1 = a1 + self._a2 = a2 + self._cos_a1 = math.cos(a1) + self._sin_a1 = math.sin(a1) + self._cos_a2 = math.cos(a2) + self._sin_a2 = math.sin(a2) + self._min = None + self._max = None + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Arc2D from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "type": "Arc2D" + "c": (10, 0), + "r": 5, + "a1": 0, + "a2": 3.14159 + } + """ + return cls(Point2D.from_array(data['c']), + data['r'], data['a1'], data['a2'])
+ + +
+[docs] + @classmethod + def from_start_mid_end(cls, p1, m, p2, circle=False): + """Initialize a new arc from start, middle, and end points. + + Note that input points will be assumed to be in counterclockwise order. + + Args: + p1: The start point of the arc. + m: Any point along the length of the arc that is not the start or end. + p2: The end point of the arc. + circle: Set to True if you would like the output to be a full circle + defined by the three points instead of an arc with a start and end. + Default is False. + """ + for pt in (p1, m, p2): + assert isinstance(pt, Point2D), "Expected Point2D. Got {}.".format(type(pt)) + e1 = (p1.x ** 2 + p1.y ** 2) + e2 = (m.x ** 2 + m.y ** 2) + e3 = (p2.x ** 2 + p2.y ** 2) + den = 2 * (p1.x * (m.y - p2.y) - p1.y * (m.x - p2.x) + m.x * p2.y - p2.x * m.y) + try: + x = -(e1 * (p2.y - m.y) + e2 * (p1.y - p2.y) + e3 * (m.y - p1.y)) / den + y = -(e1 * (m.x - p2.x) + e2 * (p2.x - p1.x) + e3 * (p1.x - m.x)) / den + except ZeroDivisionError: + raise ValueError('Input points {}, {}, {} are colinear and ' + 'cannot define an arc.'.format(p1, m, p2)) + r = math.sqrt((x - p1.x) ** 2 + (y - p1.y) ** 2) + if circle is True: + return cls(Point2D(x, y), r) + else: + a1 = Vector2D(1, 0).angle_counterclockwise(Vector2D(p1.x - x, p1.y - y)) + a2 = Vector2D(1, 0).angle_counterclockwise(Vector2D(p2.x - x, p2.y - y)) + return cls(Point2D(x, y), r, a1, a2)
+ + + @property + def c(self): + """Center point of the circle on which the arc lies.""" + return self._c + + @property + def r(self): + """Radius of arc.""" + return self._r + + @property + def a1(self): + """Start angle of the arc in radians.""" + return self._a1 + + @property + def a2(self): + """End angle of the arc in radians.""" + return self._a2 + + @property + def p1(self): + """Start point.""" + return Point2D( + self.c.x + self._cos_a1 * self.r, self.c.y + self._sin_a1 * self.r) + + @property + def p2(self): + """End point.""" + return Point2D( + self.c.x + self._cos_a2 * self.r, self.c.y + self._sin_a2 * self.r) + + @property + def midpoint(self): + """Midpoint.""" + return self.point_at(0.5) + + @property + def min(self): + """A Point2D for the minimum bounding rectangle vertex around this geometry.""" + if self._min is None: + self._calculate_min_max() + return self._min + + @property + def max(self): + """A Point2D for the maximum bounding rectangle vertex around this geometry.""" + if self._max is None: + self._calculate_min_max() + return self._max + + @property + def length(self): + """The length of the arc.""" + return self.angle * self.r + + @property + def angle(self): + """The total angle over the domain of the arc in radians.""" + _diff = self._a2 - self._a1 + return _diff if not self.is_inverted else 2 * math.pi + _diff + + @property + def area(self): + """Area of the circle to which the arc belongs.""" + assert self.is_circle, 'Arc must be a closed circle to access "area" property.' + return math.pi * self.r ** 2 + + @property + def is_circle(self): + """Boolean for whether the arc is a full circle (True) or not (False).""" + return self.a1 == 0 and self.a2 == 2 * math.pi + + @property + def is_inverted(self): + """Boolean noting whether the end angle a2 is smaller than the start angle a1.""" + return self._a2 < self._a1 + +
+[docs] + def move(self, moving_vec): + """Get an arc that has been moved along a vector. + + Args: + moving_vec: A Vector2D with the direction and distance to move the arc. + """ + return Arc2D(self.c.move(moving_vec), self.r, self.a1, self.a2)
+ + +
+[docs] + def rotate(self, angle, origin): + """Get a arc that is rotated counterclockwise by a certain angle. + + Args: + angle: An angle for rotation in radians. + origin: A Point2D for the origin around which the arc will + be rotated. + """ + _a1 = self.a1 + angle + _a2 = self.a2 + angle + _a1 = _a1 - 2 * math.pi if _a1 > 2 * math.pi else _a1 + _a2 = _a2 - 2 * math.pi if _a2 > 2 * math.pi else _a2 + return Arc2D(self.c.rotate(angle, origin), self.r, _a1, _a2)
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a arc reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector2D representing the normal vector for the plane across + which the arc will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point2D representing the origin from which to reflect. + """ + return Arc2D.from_start_mid_end(self.p2.reflect(normal, origin), + self.midpoint.reflect(normal, origin), + self.p1.reflect(normal, origin))
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a arc by a factor from an origin point. + + Args: + factor: A number representing how much the arc should be scaled. + origin: A Point2D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0). + """ + return Arc2D(self.c.scale(factor, origin), self.r * factor, self.a1, self.a2)
+ + +
+[docs] + def subdivide(self, distances): + """Get Point2D values along the arc that subdivide it based on input distances. + + Args: + distances: A list of distances along the arc at which to subdivide it. + This can also be a single number that will be repeated until the + end of the arc. + """ + if isinstance(distances, (float, int)): + distances = [distances] + arc_length = self.length + dist = distances[0] + index = 0 + sub_pts = [self.p1] + while dist < arc_length: + sub_pts.append(self.point_at_length(dist)) + if index < len(distances) - 1: + index += 1 + dist += distances[index] + sub_pts.append(self.p2) + return sub_pts
+ + +
+[docs] + def subdivide_evenly(self, number): + """Get Point2D values along the arc that divide it into evenly-spaced segments. + + Args: + number: The number of segments into which the arc will be divided. + """ + interval = 1 / number + parameter = interval + sub_pts = [self.p1] + while parameter <= 1.000000001: + sub_pts.append(self.point_at(parameter)) + parameter += interval + return sub_pts
+ + +
+[docs] + def point_at(self, parameter): + """Get a point at a given fraction along the arc. + + Args: + parameter: The fraction between the start and end point where the + desired point lies. For example, 0.5 will yield the midpoint. + """ + _ang = self._a1 + self.angle * parameter + _ang = _ang if _ang <= math.pi * 2 else _ang - math.pi * 2 + return Point2D( + self.c.x + math.cos(_ang) * self.r, self.c.y + math.sin(_ang) * self.r)
+ + +
+[docs] + def point_at_angle(self, angle): + """Get a point at a given angle along the arc. + + Args: + angle: The angle in radians from the start point along the arc + to get the Point2D. + """ + _ang = self._a1 + angle + _ang = _ang if _ang <= math.pi * 2 else _ang - math.pi * 2 + return Point2D( + self.c.x + math.cos(_ang) * self.r, self.c.y + math.sin(_ang) * self.r)
+ + +
+[docs] + def point_at_length(self, length): + """Get a point at a given distance along the arc segment. + + Args: + length: The distance along the arc from the start point where the + desired point lies. + """ + return self.point_at(length / self.length)
+ + +
+[docs] + def closest_point(self, point): + """Get the closest Point2D on this object to another Point2D. + + Args: + point: A Point2D object to which the closest point on this object + will be computed. + + Returns: + Point2D for the closest point on this line to the input point. + """ + return closest_point2d_on_arc2d(point, self)
+ + +
+[docs] + def distance_to_point(self, point): + """Get the minimum distance between this object and the input point. + + Args: + point: A Point2D object to which the minimum distance will be computed. + + Returns: + The distance to the input point. + """ + close_pt = self.closest_point(point) + return point.distance_to_point(close_pt)
+ + +
+[docs] + def intersect_line_ray(self, line_ray): + """Get the intersection between this Arc2D and another Ray2 or LineSegment2D. + + Args: + line_ray: Another LineSegment2D or Ray2D or to intersect. + + Returns: + A list of 2 Point2D objects if a full intersection exists. + A list with a single Point2D object if the line is tangent or intersects + only once. None if no intersection exists. + """ + return intersect_line2d_arc2d(line_ray, self)
+ + +
+[docs] + def intersect_line_infinite(self, line_ray): + """Get the intersection between this Arc2D and an infinitely extending Ray2D. + + Args: + line_ray: Another LineSegment2D or Ray2D or to intersect. + + Returns: + A list of 2 Point2D objects if a full intersection exists. + A list with a single Point2D object if the line is tangent or intersects + only once. None if no intersection exists. + """ + return intersect_line2d_infinite_arc2d(line_ray, self)
+ + +
+[docs] + def split_line_infinite(self, line_ray): + """Split this Arc2D in 2-3 using an infinitely extending Ray2D or LineSegment2D. + + Args: + line_ray: A LineSegment2D or Ray2D that will be extended infinitely for + intersection. + + Returns: + A list with 2 or 3 Arc2D objects if the split was successful. + Will be a list with 1 Arc2D if no intersection exists. + """ + inters = intersect_line2d_infinite_arc2d(line_ray, self) + if inters is None: + return [self] + elif self.is_circle: + if len(inters) != 2: + return [self] + a1 = self._a_from_pt(inters[0]) + a2 = self._a_from_pt(inters[1]) + return [Arc2D(self.c, self.r, a1, a2), Arc2D(self.c, self.r, a2, a1)] + elif len(inters) == 1: + am = self._a_from_pt(inters[0]) + return [Arc2D(self.c, self.r, self.a1, am), + Arc2D(self.c, self.r, am, self.a2)] + elif len(inters) == 2: + am1 = self._a_from_pt(inters[0]) + am2 = self._a_from_pt(inters[1]) + if self._cc_difference(am1) < self._cc_difference(am2): + return [Arc2D(self.c, self.r, self.a1, am1), + Arc2D(self.c, self.r, am1, am2), + Arc2D(self.c, self.r, am2, self.a2)] + else: + return [Arc2D(self.c, self.r, self.a1, am2), + Arc2D(self.c, self.r, am2, am1), + Arc2D(self.c, self.r, am1, self.a2)]
+ + +
+[docs] + def to_polyline(self, divisions, interpolated=True): + """Get this Arc2D as an approximated Polyline2D. + + Args: + divisions: The number of segments into which the arc will be divided. + interpolated: Boolean to note whether the polyline should be interpolated + between the input vertices when it is translated to other interfaces. + This property has no effect on the geometric calculations performed + by this library and is only present in order to assist with + display/translation. (Default: True) + """ + pts = self.subdivide_evenly(divisions) + return Polyline2D(pts, interpolated)
+ + +
+[docs] + def to_dict(self): + """Get Arc2D as a dictionary.""" + return {'type': 'Arc2D', 'c': self.c.to_array(), + 'r': self.r, 'a1': self.a1, 'a2': self.a2}
+ + +
+[docs] + def duplicate(self): + """Get a copy of this object.""" + return self.__copy__()
+ + + def _pt_in(self, point): + if self.is_circle: + return True + else: + v = Vector2D(point.x - self.c.x, point.y - self.c.y) + a = Vector2D(1, 0).angle_counterclockwise(v) + return (not self.is_inverted and self.a1 < a < self.a2) or \ + (self.is_inverted and self.a1 > a > self.a2) + + def _a_from_pt(self, point): + """Get the angle along the arc given a point along the arc.""" + v = Vector2D(point.x - self.c.x, point.y - self.c.y) + return Vector2D(1, 0).angle_counterclockwise(v) + + def _cc_difference(self, angle): + """Get counterclockwise different between an angle and the start of this arc.""" + _diff = angle - self.a1 + return _diff if not angle < self.a1 else 2 * math.pi + _diff + + def _calculate_min_max(self): + """Calculate maximum and minimum Point2D for this object.""" + # get the quadrants of the start and end of the arc + start_quad = self._angle_quadrant(self._a1) + end_quad = self._angle_quadrant(self._a2) + # get the min and max of the start and end points + x_cor = (self._cos_a1 * self.r, self._cos_a2 * self.r) + y_cor = (self._sin_a1 * self.r, self._sin_a2 * self.r) + mnx, mny = min(x_cor), min(y_cor) + mxx, mxy = max(x_cor), max(y_cor) + # build extremum matrices + r = self.r + x_max = ((mxx, r, r, r), (mxx, mxx, r, r), + (mxx, mxx, mxx, r), (mxx, mxx, mxx, mxx)) + y_max = ((mxy, mxy, mxy, mxy), (r, mxy, r, r), + (r, mxy, mxy, r), (r, mxy, mxy, mxy)) + x_min = ((mnx, -r, mnx, mnx), (mnx, mnx, mnx, mnx), + (-r, -r, mnx, -r), (-r, -r, mnx, mnx)) + y_min = ((mny, -r, -r, mny), (mny, mny, -r, mny), + (mny, mny, mny, mny), (-r, -r, -r, mny)) + # select the desired values from the extremum matrices + min_pt = (x_min[end_quad][start_quad], y_min[end_quad][start_quad]) + max_pt = (x_max[end_quad][start_quad], y_max[end_quad][start_quad]) + self._min = Point2D(min_pt[0] + self.c.x, min_pt[1] + self.c.y) + self._max = Point2D(max_pt[0] + self.c.x, max_pt[1] + self.c.y) + + @staticmethod + def _angle_quadrant(angle): + """Get the quadrant of a given angle in radians.""" + if angle < math.pi / 2: + return 0 + elif angle < math.pi: + return 1 + elif angle < math.pi * (3 / 2): + return 2 + return 3 + + def __copy__(self): + return Arc2D(self.c, self.r, self.a1, self.a2) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self.c, self.r, self.a1, self.a2) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Arc2D) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + +
+[docs] + def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__()
+ + + def __repr__(self): + return 'Arc2D (center <%.2f, %.2f>) (radius <%.2f>) (length <%.2f>)' % \ + (self.c.x, self.c.y, self.r, self.length)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry2d/line.html b/docs/_modules/ladybug_geometry/geometry2d/line.html new file mode 100644 index 00000000..0fe499ab --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry2d/line.html @@ -0,0 +1,1520 @@ + + + + + + + ladybug_geometry.geometry2d.line — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry2d.line

+# coding=utf-8
+"""2D Line Segment"""
+from __future__ import division
+import math
+
+from .pointvector import Vector2D, Point2D
+from ._1d import Base1DIn2D
+from ..intersection2d import closest_point2d_between_line2d, intersect_line2d, \
+    intersect_line_segment2d
+
+
+
+[docs] +class LineSegment2D(Base1DIn2D): + """2D line segment object. + + Args: + p: A Point2D representing the first point of the line segment. + v: A Vector2D representing the vector to the second point. + + Properties: + * p + * v + * p1 + * p2 + * min + * max + * center + * midpoint + * endpoints + * length + * vertices + """ + __slots__ = () + + def __init__(self, p, v): + """Initialize LineSegment2D.""" + Base1DIn2D.__init__(self, p, v) + +
+[docs] + @classmethod + def from_end_points(cls, p1, p2): + """Initialize a line segment from a start point and and end point. + + Args: + p1: A Point2D representing the first point of the line segment. + p2: A Point2D representing the second point of the line segment. + """ + v = p2 - p1 + return cls(p1, Vector2D(v.x, v.y))
+ + +
+[docs] + @classmethod + def from_sdl(cls, s, d, length): + """Initialize a line segment from a start point, direction, and length. + + Args: + s: A Point2D representing the start point of the line segment. + d: A Vector2D representing the direction of the line segment. + length: A number representing the length of the line segment. + """ + return cls(s, d * length / d.magnitude)
+ + +
+[docs] + @classmethod + def from_array(cls, line_array): + """ Create a LineSegment2D from a nested array of two endpoint coordinates. + + Args: + line_array: Nested tuples ((pt1.x, pt1.y), (pt2.x, pt2.y)), where + pt1 and pt2 represent the endpoints of the line segment. + """ + return LineSegment2D.from_end_points(*tuple(Point2D(*pt) for pt in line_array))
+ + + @property + def p1(self): + """First point (same as p).""" + return self.p + + @property + def p2(self): + """Second point.""" + return Point2D(self.p.x + self.v.x, self.p.y + self.v.y) + + @property + def midpoint(self): + """Midpoint.""" + return self.point_at(0.5) + + @property + def endpoints(self): + """Tuple of endpoints """ + return (self.p1, self.p2) + + @property + def length(self): + """The length of the line segment.""" + return self.v.magnitude + + @property + def vertices(self): + """Tuple of both vertices in this object.""" + return (self.p1, self.p2) + +
+[docs] + def is_equivalent(self, other, tolerance): + """Boolean noting equivalence (within tolerance) between this line and another. + + The order of the line points do not matter for equivalence to be true. + + Args: + other: LineSegment2D for comparison. + tolerance: float representing point equivalence. + + Returns: + True if equivalent else False + """ + tol = tolerance + return ( + self.p1.is_equivalent(other.p1, tol) and self.p2.is_equivalent(other.p2, tol) + ) or ( + self.p1.is_equivalent(other.p2, tol) and self.p2.is_equivalent(other.p1, tol) + )
+ + +
+[docs] + def flip(self): + """Get a copy of this line segment that is flipped.""" + return LineSegment2D(self.p2, self.v.reverse())
+ + +
+[docs] + def move(self, moving_vec): + """Get a line segment that has been moved along a vector. + + Args: + moving_vec: A Vector2D with the direction and distance to move the ray. + """ + return LineSegment2D(self.p.move(moving_vec), self.v)
+ + +
+[docs] + def rotate(self, angle, origin): + """Get a line segment that is rotated counterclockwise by a certain angle. + + Args: + angle: An angle for rotation in radians. + origin: A Point2D for the origin around which the line segment will + be rotated. + """ + return LineSegment2D(self.p.rotate(angle, origin), self.v.rotate(angle))
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a line segment reflected across a plane with the input normal and origin. + + Args: + normal: A Vector2D representing the normal vector for the plane across + which the line segment will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point2D representing the origin from which to reflect. + """ + return LineSegment2D(self.p.reflect(normal, origin), self.v.reflect(normal))
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a line segment by a factor from an origin point. + + Args: + factor: A number representing how much the line segment should be scaled. + origin: A Point2D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0). + """ + return LineSegment2D(self.p.scale(factor, origin), self.v * factor)
+ + +
+[docs] + def offset(self, distance): + """Offset the line segment by a given distance. + + Args: + distance: The distance that the line segment will be offset. Both positive + and negative values are accepted with positive values being offset + to the left of the line and negative values being offset to the + right of the line (starting from LineSegment.p and looking + in the direction of LineSegment.v). + """ + if distance == 0: + return self + move_vec = self.v.rotate(math.pi / 2).normalize() * distance + return LineSegment2D(self.p.move(move_vec), self.v)
+ + +
+[docs] + def subdivide(self, distances): + """Get Point2D values along the line that subdivide it based on input distances. + + Args: + distances: A list of distances along the line at which to subdivide it. + This can also be a single number that will be repeated until the + end of the line. + """ + if isinstance(distances, (float, int)): + distances = [distances] + line_length = self.length + dist = distances[0] + index = 0 + sub_pts = [self.p] + while dist < line_length: + sub_pts.append(self.point_at_length(dist)) + if index < len(distances) - 1: + index += 1 + dist += distances[index] + sub_pts.append(self.p2) + return sub_pts
+ + +
+[docs] + def subdivide_evenly(self, number): + """Get Point2D values along the line that divide it into evenly-spaced segments. + + Args: + number: The number of segments into which the line will be divided. + """ + interval = 1 / number + parameter = interval + sub_pts = [self.p] + while parameter <= 1: + sub_pts.append(self.point_at(parameter)) + parameter += interval + return sub_pts
+ + +
+[docs] + def point_at(self, parameter): + """Get a point at a given fraction along the line segment. + + Args: + parameter: The fraction between the start and end point where the + desired point lies. For example, 0.5 will yield the midpoint. + """ + return self.p + self.v * parameter
+ + +
+[docs] + def point_at_length(self, length): + """Get a point at a given distance along the line segment. + + Args: + length: The distance along the line from the start point where the + desired point lies. + """ + return self.p + self.v * (length / self.length)
+ + +
+[docs] + def intersect_line_ray(self, line_ray): + """Get the intersection between this object and another Ray2 or LineSegment2D. + + Args: + line_ray: Another LineSegment2D or Ray2D or to intersect. + + Returns: + Point2D of intersection if it exists. None if no intersection exists. + """ + if isinstance(line_ray, LineSegment2D): + return intersect_line_segment2d(self, line_ray) + return intersect_line2d(self, line_ray)
+ + +
+[docs] + def closest_points_between_line(self, line): + """Get the two closest Point2D between this object to another LineSegment2D. + + Note that the line segments should not intersect for the result to be valid. + + Args: + line: A LineSegment2D object to which the closest points + will be computed. + + Returns: + Two Point2D objects representing + + 1) The closest point on this object to the input line. + 2) The closest point on the input line to this object. + """ + dist, pts = closest_point2d_between_line2d(self, line) + return pts
+ + +
+[docs] + def distance_to_line(self, line): + """Get the minimum distance between this object and the input LineSegment2D. + + Note that the line segments should not intersect for the result to be valid. + + Args: + line: A LineSegment2D object to which the minimum distance will be computed. + + Returns: + The minimum distance to the input line. + """ + dist, pts = closest_point2d_between_line2d(self, line) + return dist
+ + +
+[docs] + def to_dict(self): + """Get LineSegment2D as a dictionary.""" + base = Base1DIn2D.to_dict(self) + base['type'] = 'LineSegment2D' + return base
+ + +
+[docs] + def to_array(self): + """ A nested list representing the two line endpoint coordinates.""" + return (self.p1.to_array(), self.p2.to_array())
+ + + def _u_in(self, u): + return u >= 0.0 and u <= 1.0 + + def __abs__(self): + return abs(self.v) + + def __copy__(self): + return LineSegment2D(self.p, self.v) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (hash(self.p), hash(self.v)) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, LineSegment2D) and self.__key() == other.__key() + + def __repr__(self): + return 'LineSegment2D (<%.2f, %.2f> to <%.2f, %.2f>)' % \ + (self.p.x, self.p.y, self.p.x + self.v.x, self.p.y + self.v.y)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry2d/mesh.html b/docs/_modules/ladybug_geometry/geometry2d/mesh.html new file mode 100644 index 00000000..48df40db --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry2d/mesh.html @@ -0,0 +1,1961 @@ + + + + + + + ladybug_geometry.geometry2d.mesh — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry2d.mesh

+# coding=utf-8
+"""2D Mesh"""
+from __future__ import division
+try:
+    from itertools import izip as zip  # python 2
+except ImportError:
+    xrange = range  # python 3
+
+from .._mesh import MeshBase
+from ..triangulation import earcut
+
+from .pointvector import Point2D, Vector2D
+from .line import LineSegment2D
+from .polyline import Polyline2D
+from .polygon import Polygon2D
+
+
+
+[docs] +class Mesh2D(MeshBase): + """2D Mesh object. + + Args: + vertices: A list or tuple of Point2D objects for vertices. + faces: A list of tuples with each tuple having either 3 or 4 integers. + These integers correspond to indices within the list of vertices. + colors: An optional list of colors that correspond to either the faces + of the mesh or the vertices of the mesh. Default is None. + + Properties: + * vertices + * faces + * colors + * is_color_by_face + * min + * max + * center + * area + * centroid + * face_areas + * face_centroids + * face_area_centroids + * face_vertices + * vertex_connected_faces + * edges + * naked_edges + * internal_edges + * non_manifold_edges + """ + __slots__ = ('_min', '_max', '_center', '_centroid') + + def __init__(self, vertices, faces, colors=None): + """Initialize Mesh2D.""" + self._vertices = self._check_vertices_input(vertices) + self._faces = self._check_faces_input(faces) + self._is_color_by_face = False # default if colors is None + self.colors = colors + + self._min = None + self._max = None + self._center = None + self._area = None + self._centroid = None + self._face_areas = None + self._face_centroids = None + self._face_area_centroids = None + self._vertex_connected_faces = None + self._edge_indices = None + self._edge_types = None + self._edges = None + self._naked_edges = None + self._internal_edges = None + self._non_manifold_edges = None + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Mesh2D from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "type": "Mesh2D", + "vertices": [(0, 0), (10, 0), (0, 10)], + "faces": [(0, 1, 2)], + "colors": [{"r": 255, "g": 0, "b": 0}] + } + """ + colors = None + if 'colors' in data and data['colors'] is not None and len(data['colors']) != 0: + try: + from ladybug.color import Color + except ImportError: + raise ImportError('Colors are specified in input Mesh2D dictionary ' + 'but failed to import ladybug.color') + colors = tuple(Color.from_dict(col) for col in data['colors']) + fcs = tuple(tuple(f) for f in data['faces']) # cast to immutable type + return cls(tuple(Point2D.from_array(pt) for pt in data['vertices']), fcs, colors)
+ + +
+[docs] + @classmethod + def from_face_vertices(cls, faces, purge=True): + """Create a mesh from a list of faces with each face defined by Point2Ds. + + Args: + faces: A list of faces with each face defined as a list of 3 or 4 Point2D. + purge: A boolean to indicate if duplicate vertices should be shared between + faces. Default is True to purge duplicate vertices, which can be slow + for large lists of faces but results in a higher-quality mesh with + a smaller size in memory. Note that vertices are only considered + duplicate if the coordinate values are equal to one another + within floating point tolerance. To remove duplicate vertices + within a specified tolerance other than floating point, the + from_purged_face_vertices method should be used instead. + """ + vertices, face_collector = cls._interpret_input_from_face_vertices(faces, purge) + return cls(tuple(vertices), tuple(face_collector))
+ + +
+[docs] + @classmethod + def from_purged_face_vertices(cls, faces, tolerance): + """Create a mesh from a list of faces with each face defined by Point3Ds. + + This method is slower than 'from_face_vertices' but will result in a mesh + with fewer vertices and a smaller size in memory. This method is similar to + using the 'purge' option in 'from_face_vertices' but will result in more shared + vertices since it uses a tolerance to check equivalent vertices rather than + comparing within floating point tolerance. + + Args: + faces: A list of faces with each face defined as a list of 3 or 4 Point3D. + tolerance: A number for the minimum difference between coordinate + values at which point vertices are considered equal to one another. + """ + vertices, faces = cls._interpret_input_from_face_vertices_with_tolerance( + faces, tolerance) + return cls(tuple(vertices), tuple(faces))
+ + +
+[docs] + @classmethod + def from_polygon_triangulated(cls, boundary_polygon, hole_polygons=None): + """Initialize a triangulated Mesh2D from a Polygon2D. + + The triangles of the mesh faces will always completely fill the shape + defines by the input boundary_polygon with holes subtracted from it. + + Args: + boundary_polygon: A Polygon2D object representing the boundary of the shape. + hole_polygons: Optional list of Polygon2D objects representing holes + within the boundary_polygon. + """ + assert isinstance(boundary_polygon, Polygon2D), 'boundary_polygon must be a ' \ + 'Polygon2D to use from_polygon_triangulated. Got {}.'.format( + type(boundary_polygon)) + + if hole_polygons is None and boundary_polygon.is_convex: # fan triangulation! + _faces = [] + for i in xrange(1, len(boundary_polygon) - 1): + _faces.append((0, i, i + 1)) + _new_mesh = cls(boundary_polygon.vertices, _faces) + else: # slower ear-clipping method + if hole_polygons is not None: + for hole in hole_polygons: + assert isinstance(hole, Polygon2D), 'Hole must be a Polygon2D ' \ + 'to use from_polygon_triangulated. Got {}.'.format(type(hole)) + _vertices, _faces = Mesh2D._ear_clipping_triangulation( + boundary_polygon, hole_polygons) + _new_mesh = cls(_vertices, _faces) + + return _new_mesh
+ + +
+[docs] + @classmethod + def from_polygon_grid(cls, polygon, x_dim, y_dim, generate_centroids=True): + """Initialize a gridded Mesh2D from a Polygon2D. + + Note that this gridded mesh will usually not completely fill the polygon. + Essentially, this method generates a grid over the domain of the polygon + and then removes any points that do not lie within the polygon. + + Args: + polygon: A Polygon2D object. + x_dim: The x dimension of the grid cells as a number. + y_dim: The y dimension of the grid cells as a number. + generate_centroids: Set to True to have the face centroids generated + alongside the grid of vertices, which is much faster than having + them generated upon request as they typically are. However, if you + have no need for the face centroids, you would save memory by setting + this to False. Default is True. + """ + assert isinstance(polygon, Polygon2D), 'Expected Polygon2D for' \ + ' Mesh2D.from_polygon_grid. Got {}'.format(type(polygon)) + # figure out how many x and y cells to make + _x_dim, _num_x = Mesh2D._domain_dimensions(polygon.max.x - polygon.min.x, x_dim) + _y_dim, _num_y = Mesh2D._domain_dimensions(polygon.max.y - polygon.min.y, y_dim) + _poly_min = polygon.min + + # generate the gid of points and faces + _verts = Mesh2D._grid_vertices(_poly_min, _num_x, _num_y, _x_dim, _y_dim) + _faces = Mesh2D._grid_faces(_num_x, _num_y) + _centroids = None + if generate_centroids is True: # calculate centroids if requested + _centroids = Mesh2D._grid_centroids( + _poly_min, _num_x, _num_y, _x_dim, _y_dim) + + # figure out which vertices lie inside the polygon + # for tolerance reasons, we scale the polygon by a very small amount + # this avoids the fringe cases noted in the Polygon2d.is_point_inside description + tol_pt = Vector2D(0.0000001, 0.0000001) + scaled_poly = Polygon2D( + tuple(pt.scale(1.000001, _poly_min) - tol_pt for pt in polygon.vertices)) + _pattern = [scaled_poly.is_point_inside(_v) for _v in _verts] + + # build the mesh + _mesh_init = cls(_verts, _faces) + _mesh_init._face_centroids = _centroids + _mesh_init._face_area_centroids = _centroids + _new_mesh, _face_pattern = _mesh_init.remove_vertices(_pattern) + _new_mesh._face_areas = x_dim * y_dim + return _new_mesh
+ + +
+[docs] + @classmethod + def from_grid(cls, base_point=Point2D(), num_x=1, num_y=1, x_dim=1, y_dim=1, + generate_centroids=True): + """Initialize a Mesh2D from parameters that define a grid. + + Args: + base_point: The base point from which the mesh grid will be generated. + Default is (0, 0). + num_x: An integer for the number of mesh cells to generate in the + x direction. Default is 1. + num_y: An integer for the number of mesh cells to generate in the + y direction. Default is 1. + x_dim: The x dimension of the grid cells as a number. Default is 1. + y_dim: The y dimension of the grid cells as a number. Default is 1. + generate_centroids: Set to True to have the face centroids generated + alongside the grid of vertices, which is much faster than having + them generated upon request as they typically are. However, if you + have no need for the face centroids, you would save memory by setting + this to False. Default is True. + """ + _verts = Mesh2D._grid_vertices(base_point, num_x, num_y, x_dim, y_dim) + _faces = Mesh2D._grid_faces(num_x, num_y) + _centroids = None + if generate_centroids is True: + _centroids = Mesh2D._grid_centroids(base_point, num_x, num_y, x_dim, y_dim) + + _new_mesh = cls(tuple(_verts), tuple(_faces)) + _new_mesh._face_areas = x_dim * y_dim + _new_mesh._face_centroids = _centroids + _new_mesh._face_area_centroids = _centroids + return _new_mesh
+ + + @property + def min(self): + """A Point2D for the minimum bounding rectangle vertex around this geometry.""" + if self._min is None: + self._calculate_min_max() + return self._min + + @property + def max(self): + """A Point2D for the maximum bounding rectangle vertex around this geometry.""" + if self._max is None: + self._calculate_min_max() + return self._max + + @property + def center(self): + """A Point2D for the center of the bounding rectangle around this geometry.""" + if self._center is None: + min, max = self.min, self.max + self._center = Point2D((min.x + max.x) / 2, (min.y + max.y) / 2) + return self._center + + @property + def face_areas(self): + """A tuple of face areas that parallels the faces property.""" + if self._face_areas is None: + self._face_areas = tuple(self._face_area(face) for face in self.faces) + elif isinstance(self._face_areas, (float, int)): # grid of faces with same area + self._face_areas = tuple(self._face_areas for face in self.faces) + return self._face_areas + + @property + def centroid(self): + """The centroid of the mesh as a Point2D (aka. center of mass). + + Note that the centroid is more time consuming to compute than the center + (or the middle point of the bounding rectangle). So the center might be + preferred over the centroid if you just need a rough point for the middle + of the mesh. + """ + if self._centroid is None: + _weight_x = 0 + _weight_y = 0 + for _c, _a in zip(self.face_area_centroids, self.face_areas): + _weight_x += _c.x * _a + _weight_y += _c.y * _a + self._centroid = Point2D(_weight_x / self.area, _weight_y / self.area) + return self._centroid + + @property + def face_edges(self): + """List of polylines with one Polyline2D for each face. + + This is faster to compute compared to the edges and results in effectively + the same type of wireframe visualization. + """ + _all_verts = self._vertices + f_edges = [] + for face in self._faces: + verts = tuple(_all_verts[v] for v in face) + (_all_verts[face[0]],) + f_edges.append(Polyline2D(verts)) + return f_edges + + @property + def edges(self): + """"Tuple of all edges in this Mesh3D as LineSegment3D objects.""" + if self._edges is None: + if self._edge_indices is None: + self._compute_edge_info() + self._edges = tuple(LineSegment2D.from_end_points( + self.vertices[seg[0]], self.vertices[seg[1]]) + for seg in self._edge_indices) + return self._edges + + @property + def naked_edges(self): + """"Tuple of all naked edges in this Mesh3D as LineSegment3D objects. + + Naked edges belong to only one face in the mesh (they are not + shared between faces). + """ + if self._naked_edges is None: + self._naked_edges = self._get_edge_type(0) + return self._naked_edges + + @property + def internal_edges(self): + """"Tuple of all internal edges in this Mesh3D as LineSegment3D objects. + + Internal edges are shared between two faces in the mesh. + """ + if self._internal_edges is None: + self._internal_edges = self._get_edge_type(1) + return self._internal_edges + + @property + def non_manifold_edges(self): + """"Tuple of all non-manifold edges in this mesh as LineSegment3D objects. + + Non-manifold edges are shared between three or more faces. + """ + if self._non_manifold_edges is None: + if self._edges is None: + self.edges + nm_edges = [] + for i, type in enumerate(self._edge_types): + if type > 1: + nm_edges.append(self._edges[i]) + self._non_manifold_edges = tuple(nm_edges) + return self._non_manifold_edges + +
+[docs] + def triangulated(self): + """Get a version of this Mesh2D where all quads have been triangulated.""" + _new_faces = [] + for face in self.faces: + if len(face) == 3: + _new_faces.append(face) + else: + _triangles = Mesh2D._quad_to_triangles([self._vertices[i] for i in face]) + _triangles = [tuple(face[vertex_idx] for vertex_idx in new_face) + for new_face in _triangles] + _new_faces.extend(_triangles) + _new_faces = tuple(_new_faces) + + _new_colors = self.colors + if self.is_color_by_face is True: + _new_colors = [] + for i, face in enumerate(self.faces): + if len(face) == 3: + _new_colors.append(self.colors[i]) + else: + _new_colors.extend([self.colors[i]] * 2) + _new_colors = tuple(_new_colors) + + _new_mesh = Mesh2D(self.vertices, _new_faces, _new_colors) + return _new_mesh
+ + +
+[docs] + def remove_vertices(self, pattern): + """Get a version of this mesh where vertices are removed according to a pattern. + + Args: + pattern: A list of boolean values denoting whether a vertex should + remain in the mesh (True) or be removed from the mesh (False). + The length of this list must match the number of this mesh's vertices. + + Returns: + A tuple with two elements + + - new_mesh: + A mesh where the vertices have been removed according + to the input pattern. + + - face_pattern: + A list of boolean values that corresponds to the + original mesh faces noting whether the face is in the new mesh (True) + or has been removed from the new mesh (False). + """ + _new_verts, _new_faces, _new_colors, _new_f_cent, _new_f_area, face_pattern = \ + self._remove_vertices(pattern) + + new_mesh = Mesh2D(_new_verts, _new_faces, _new_colors) + new_mesh._face_centroids = _new_f_cent + new_mesh._face_areas = _new_f_area + return new_mesh, face_pattern
+ + +
+[docs] + def remove_faces(self, pattern): + """Get a version of this mesh where faces are removed according to a pattern. + + Args: + pattern: A list of boolean values denoting whether a face should + remain in the mesh (True) or be removed from the mesh (False). + The length of this list must match the number of this mesh's faces. + + Returns: + A tuple with two elements + + - new_mesh: + A mesh where the faces have been removed according + to the input pattern. + + - vertex_pattern: + A list of boolean values that corresponds to the + original mesh vertices noting whether the vertex is in the new mesh + (True) or has been removed from the new mesh (False). + """ + vertex_pattern = self._vertex_pattern_from_remove_faces(pattern) + _new_verts, _new_faces, _new_colors, _new_f_cent, _new_f_area, face_pattern = \ + self._remove_vertices(vertex_pattern, pattern) + + new_mesh = Mesh2D(_new_verts, _new_faces, _new_colors) + new_mesh._face_centroids = _new_f_cent + new_mesh._face_areas = _new_f_area + return new_mesh, vertex_pattern
+ + +
+[docs] + def remove_faces_only(self, pattern): + """Get a version of this mesh where faces are removed and vertices are unaltered. + + This is faster than the Mesh2D.remove_faces method but will likely result + a lower-quality mesh where several vertices exist in the mesh that are not + referenced by any face. This may be preferred if pure speed of removing + faces is a priority over smallest size of the mesh in memory. + + Args: + pattern: A list of boolean values denoting whether a face should + remain in the mesh (True) or be removed from the mesh (False). + The length of this list must match the number of this mesh's faces. + + Returns: + new_mesh -- A mesh where the faces have been removed according + to the input pattern. + """ + _new_faces, _new_colors, _new_f_cent, _new_f_area = \ + self._remove_faces_only(pattern) + + new_mesh = Mesh2D(self.vertices, _new_faces, _new_colors) + new_mesh._face_centroids = _new_f_cent + new_mesh._face_areas = _new_f_area + return new_mesh
+ + +
+[docs] + def rotate(self, angle, origin): + """Get a mesh that is rotated counterclockwise by a certain angle. + + Args: + angle: An angle for rotation in radians. + origin: A Point2D for the origin around which the point will be rotated. + """ + _verts = tuple([pt.rotate(angle, origin) for pt in self.vertices]) + return self._mesh_transform(_verts)
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a mesh by a factor from an origin point. + + Args: + factor: A number representing how much the mesh should be scaled. + origin: A Point representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0). + """ + if origin is None: + _verts = tuple( + Point2D(pt.x * factor, pt.y * factor) for pt in self.vertices) + else: + _verts = tuple(pt.scale(factor, origin) for pt in self.vertices) + return self._mesh_scale(_verts, factor)
+ + +
+[docs] + def to_dict(self): + """Get Mesh2D as a dictionary.""" + colors = None + if self.colors is not None: + colors = [col.to_dict() for col in self.colors] + return {'type': 'Mesh2D', + 'vertices': [pt.to_array() for pt in self.vertices], + 'faces': self.faces, 'colors': colors}
+ + +
+[docs] + @staticmethod + def join_meshes(meshes): + """Join an array of Mesh2Ds into a single Mesh2D. + + Args: + meshes: An array of meshes to be joined into one. + + Returns: + A single Mesh2D object derived from the input meshes. + """ + # set up empty lists of objects to be filled + verts = [] + faces = [] + colors = [] + + # loop through all of the meshes and get new faces + total_v_i = 0 + for mesh in meshes: + verts.extend(mesh._vertices) + for fc in mesh._faces: + faces.append(tuple(v_i + total_v_i for v_i in fc)) + total_v_i += len(mesh._vertices) + if mesh._colors: + colors.extend(mesh._colors) + + # create the new mesh + if len(colors) != 0: + new_mesh = Mesh2D(verts, faces, colors) + else: + new_mesh = Mesh2D(verts, faces) + + # attempt to transfer the centroids and normals + if all(msh._face_centroids is not None for msh in meshes): + new_mesh._face_centroids = tuple(pt for msh in meshes for pt in msh) + if all(msh._face_areas is not None for msh in meshes): + new_mesh._face_areas = tuple(a for msh in meshes for a in msh.face_areas) + return new_mesh
+ + + def _calculate_min_max(self): + """Calculate maximum and minimum Point2D for this object.""" + min_pt = [self.vertices[0].x, self.vertices[0].y] + max_pt = [self.vertices[0].x, self.vertices[0].y] + + for v in self.vertices[1:]: + if v.x < min_pt[0]: + min_pt[0] = v.x + elif v.x > max_pt[0]: + max_pt[0] = v.x + if v.y < min_pt[1]: + min_pt[1] = v.y + elif v.y > max_pt[1]: + max_pt[1] = v.y + + self._min = Point2D(min_pt[0], min_pt[1]) + self._max = Point2D(max_pt[0], max_pt[1]) + + def _get_edge_type(self, edge_type): + """Get all of the edges of a certain type in this mesh.""" + if self._edges is None: + self.edges + sel_edges = [] + for i, type in enumerate(self._edge_types): + if type == edge_type: + sel_edges.append(self._edges[i]) + return tuple(sel_edges) + + def _face_area(self, face): + """Return the area of a face.""" + return Mesh2D._get_area(tuple(self._vertices[i] for i in face)) + + def _tri_face_centroid(self, face): + """Compute the centroid of a triangular face.""" + return Mesh2D._tri_centroid(tuple(self._vertices[i] for i in face)) + + def _quad_face_centroid(self, face): + """Compute the centroid of a quadrilateral face.""" + return Mesh2D._quad_centroid(tuple(self._vertices[i] for i in face)) + + def _mesh_transform(self, verts): + """Transform mesh in a way that transfers properties and avoids extra checks.""" + _new_mesh = Mesh2D(verts, self.faces) + self._transfer_properties(_new_mesh) + return _new_mesh + + def _mesh_scale(self, verts, factor): + """Scale mesh in a way that transfers properties and avoids extra checks.""" + _new_mesh = Mesh2D(verts, self.faces) + self._transfer_properties_scale(_new_mesh, factor) + return _new_mesh + + def _check_vertices_input(self, vertices): + if not isinstance(vertices, tuple): + vertices = tuple(vertices) + for vert in vertices: + assert isinstance(vert, Point2D), \ + 'Expected Point2D for {} vertex. Got {}.'.format( + self.__class__.__name__, type(vert)) + return vertices + + @staticmethod + def _ear_clipping_triangulation(polygon, holes=None): + """Triangulate a polygon and holes using the ear clipping method.""" + # flatten the list of vertices and holes into a single list for earcut + vert_coords, hole_indices = [], None + for pt in polygon: + vert_coords.extend((pt.x, pt.y)) + if holes is not None: + hole_indices = [] + for hole in holes: + hole_indices.append(int(len(vert_coords) / 2)) + for pt in hole: + vert_coords.extend((pt.x, pt.y)) + + # run the ear clipping triangulation + result_tri = earcut(vert_coords, hole_indices) + vertices = tuple(Point2D(*vert_coords[st:st + 2]) + for st in range(0, len(vert_coords), 2)) + faces = tuple(tuple(result_tri[st:st + 3]) + for st in range(0, len(result_tri), 3)) + return vertices, faces + + @staticmethod + def _quad_to_triangles(verts): + """Return two triangles that represent any quadrilateral.""" + # check if the quad is convex + convex = True + pt1, pt2, pt3 = verts[1], verts[2], verts[3] + start_val = True if (pt2.x - pt1.x) * (pt3.y - pt2.y) - \ + (pt2.y - pt1.y) * (pt3.x - pt2.x) > 0 else False + for i, pt3 in enumerate(verts[:3]): + pt1 = verts[i - 2] + pt2 = verts[i - 1] + val = True if (pt2.x - pt1.x) * (pt3.y - pt2.y) - \ + (pt2.y - pt1.y) * (pt3.x - pt2.x) > 0 else False + if val is not start_val: + convex = False + break + if convex is True: + # if the quad is convex, either diagonal splits it into triangles + return [(0, 1, 2), (2, 3, 0)] + else: + # if it is concave, we need to select the right diagonal of the two + return Mesh2D._concave_quad_to_triangles(verts) + + @staticmethod + def _concave_quad_to_triangles(verts): + """Return two triangles that represent a concave quadrilateral.""" + quad_poly = Polygon2D(verts) + diagonal = LineSegment2D.from_end_points(quad_poly[0], quad_poly[2]) + if quad_poly.is_point_inside(diagonal.midpoint, Vector2D(1, 0.00001)): + # if the diagonal midpoint is inside the quad, it splits it into two ears + return [(0, 1, 2), (2, 3, 0)] + else: + # if not, then the other diagonal splits it into two ears + return [(1, 2, 3), (3, 0, 1)] + + @staticmethod + def _face_center(verts): + """Get the center of a list of Point3D vertices.""" + _cent_x = sum([v.x for v in verts]) + _cent_y = sum([v.y for v in verts]) + v_count = len(verts) + return Point2D(_cent_x / v_count, _cent_y / v_count) + + @staticmethod + def _quad_centroid(verts): + """Get the centroid of a list of 4 Point2D vertices.""" + _tri_i = Mesh2D._quad_to_triangles(verts) + _tri_verts = ([verts[i] for i in _tri_i[0]], [verts[i] for i in _tri_i[1]]) + _tri_c = [Mesh2D._tri_centroid(tri) for tri in _tri_verts] + _tri_a = [Mesh2D._get_area(tri) for tri in _tri_verts] + _tot_a = sum(_tri_a) + _cent_x = (_tri_c[0].x * _tri_a[0] + _tri_c[1].x * _tri_a[1]) / _tot_a + _cent_y = (_tri_c[0].y * _tri_a[0] + _tri_c[1].y * _tri_a[1]) / _tot_a + return Point2D(_cent_x, _cent_y) + + @staticmethod + def _tri_centroid(verts): + """Get the centroid of a list of 3 Point2D vertices.""" + _cent_x = sum([v.x for v in verts]) + _cent_y = sum([v.y for v in verts]) + return Point2D(_cent_x / 3, _cent_y / 3) + + @staticmethod + def _get_area(verts): + """Return the area of a list of Point2D vertices.""" + _a = 0 + for i, pt in enumerate(verts): + _a += verts[i - 1].x * pt.y - verts[i - 1].y * pt.x + return abs(_a / 2) + + @staticmethod + def _domain_dimensions(_dom, _dim): + """Get corrected dimensions and number of cells over a domain.""" + _num = int(_dom / _dim) + _num = 1 if _num == 0 else _num + _dim = _dom / _num + return _dim, _num + + @staticmethod + def _grid_vertices(base_point, num_x, num_y, x_dim, y_dim): + """Generate Point2D vertices for a grid.""" + _verts = [] + _x = base_point.x + for i in xrange(num_x + 1): + _y = base_point.y + for j in xrange(num_y + 1): + _verts.append(Point2D(_x, _y)) + _y += y_dim + _x += x_dim + return _verts + + @staticmethod + def _grid_faces(num_x, num_y): + """Generate face tuples for a grid.""" + _faces = [] + _c = 0 + for i in xrange(num_x): + for j in xrange(num_y): + _faces.append((_c, _c + num_y + 1, _c + num_y + 2, _c + 1)) + _c += 1 + _c += 1 + return _faces + + @staticmethod + def _grid_centroids(base_point, num_x, num_y, x_dim, y_dim): + """Generate Point2D centroids for a grid.""" + _centroids = [] + _x_half = x_dim / 2 + _y_half = y_dim / 2 + _x = base_point.x + for i in xrange(num_x): + _y = base_point.y + for j in xrange(num_y): + _centroids.append(Point2D(_x + _x_half, _y + _y_half)) + _y += y_dim + _x += x_dim + return tuple(_centroids) + + def __copy__(self): + _new_mesh = Mesh2D(self.vertices, self.faces) + self._transfer_properties(_new_mesh) + _new_mesh._face_centroids = self._face_centroids + _new_mesh._centroid = self._centroid + return _new_mesh + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return tuple(hash(pt) for pt in self._vertices) + \ + tuple(hash(face) for face in self._faces) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Mesh2D) and self.__key() == other.__key() + + def __repr__(self): + return 'Ladybug Mesh2D ({} faces) ({} vertices)'.format( + len(self.faces), len(self))
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry2d/pointvector.html b/docs/_modules/ladybug_geometry/geometry2d/pointvector.html new file mode 100644 index 00000000..9a63ac77 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry2d/pointvector.html @@ -0,0 +1,1710 @@ + + + + + + + ladybug_geometry.geometry2d.pointvector — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry2d.pointvector

+# coding=utf-8
+"""2D Vector and 2D Point"""
+from __future__ import division
+
+import math
+import operator
+
+
+
+[docs] +class Vector2D(object): + """2D Vector object. + + Args: + x: Number for the X coordinate. + y: Number for the Y coordinate. + + Properties: + * x + * y + * magnitude + * magnitude_squared + * is_zero + """ + __slots__ = ('_x', '_y') + + def __init__(self, x=0, y=0): + """Initialize 2D Vector.""" + self._x = self._cast_to_float(x) + self._y = self._cast_to_float(y) + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Vector2D/Point2D from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "x": 10, + "y": 0 + } + """ + return cls(data['x'], data['y'])
+ + +
+[docs] + @classmethod + def from_array(cls, array): + """Initialize a Vector2D/Point2D from an array. + + Args: + array: A tuple or list with two numbers representing the x and y + values of the point. + """ + return cls(array[0], array[1])
+ + +
+[docs] + def to_array(self): + """Get Vector2D/Point2D as a tuple of two numbers""" + return (self.x, self.y)
+ + + @property + def x(self): + """Get the X coordinate.""" + return self._x + + @property + def y(self): + """Get the Y coordinate.""" + return self._y + + @property + def magnitude(self): + """Get the magnitude of the vector.""" + return self.__abs__() + + @property + def magnitude_squared(self): + """Get the magnitude squared of the vector.""" + return self.x ** 2 + self.y ** 2 + + @property + def min(self): + """Always equal to (0, 0). + + This property exists to help with bounding box calculations. + """ + return Point2D(0, 0) + + @property + def max(self): + """Always equal to (0, 0). + + This property exists to help with bounding box calculations. + """ + return Point2D(0, 0) + +
+[docs] + def is_zero(self, tolerance): + """Boolean to note whether the vector is within a given zero tolerance. + + Args: + tolerance: The tolerance below which the vector is considered to + be a zero vector. + """ + return abs(self.x) <= tolerance and abs(self.y) <= tolerance
+ + +
+[docs] + def is_equivalent(self, other, tolerance): + """Test whether this object is equivalent to another within a certain tolerance. + + Note that if you want to test whether the coordinate values are perfectly + equal to one another, the == operator can be used. + + Args: + other: Another Point2D for which geometric equivalency will be tested. + tolerance: The minimum difference between the coordinate values of two + objects at which they can be considered geometrically equivalent. + Returns: + True if equivalent. False if not equivalent. + """ + return abs(self.x - other.x) <= tolerance and \ + abs(self.y - other.y) <= tolerance
+ + +
+[docs] + def normalize(self): + """Get a copy of the vector that is a unit vector (magnitude=1).""" + d = self.magnitude + try: + return Vector2D(self.x / d, self.y / d) + except ZeroDivisionError: + return self.duplicate()
+ + +
+[docs] + def reverse(self): + """Get a copy of this vector that is reversed.""" + return self.__neg__()
+ + +
+[docs] + def dot(self, other): + """Get the dot product of this vector with another.""" + return self.x * other.x + self.y * other.y
+ + +
+[docs] + def determinant(self, other): + """Get the determinant between this vector and another 2D vector.""" + return self.x * other.y - self.y * other.x
+ + +
+[docs] + def cross(self): + """Get the cross product of this vector.""" + return Vector2D(self.y, -self.x)
+ + +
+[docs] + def angle(self, other): + """Get the smallest angle between this vector and another.""" + try: + return math.acos(self.dot(other) / (self.magnitude * other.magnitude)) + except ValueError: # python floating tolerance can cause math domain error + if self.dot(other) < 0: + return math.acos(-1) + return math.acos(1)
+ + +
+[docs] + def angle_counterclockwise(self, other): + """Get the counterclockwise angle between this vector and another.""" + inner = self.angle(other) + det = self.determinant(other) + if det >= 0: + return inner # if the det > 0 then self is immediately clockwise of other + else: + return 2 * math.pi - inner # if the det < 0 then other is clockwise of self
+ + +
+[docs] + def angle_clockwise(self, other): + """Get the clockwise angle between this vector and another.""" + inner = self.angle(other) + det = self.determinant(other) + if det <= 0: + return inner # if the det > 0 then self is immediately clockwise of other + else: + return 2 * math.pi - inner # if the det < 0 then other is clockwise of self
+ + +
+[docs] + def rotate(self, angle): + """Get a vector that is rotated counterclockwise by a certain angle. + + Args: + angle: An angle for rotation in radians. + """ + return Vector2D._rotate(self, angle)
+ + +
+[docs] + def reflect(self, normal): + """Get a vector that is reflected across a plane with the input normal vector. + + Args: + normal: A Vector2D representing the normal vector for the plane across + which the vector will be reflected. THIS VECTOR MUST BE NORMALIZED. + """ + return Vector2D._reflect(self, normal)
+ + +
+[docs] + def duplicate(self): + """Get a copy of this vector.""" + return self.__copy__()
+ + +
+[docs] + def to_dict(self): + """Get Vector2D as a dictionary.""" + return {'type': 'Vector2D', + 'x': self.x, + 'y': self.y}
+ + +
+[docs] + @staticmethod + def circular_mean(angles): + """Compute the circular mean across a list of angles in radians. + + If no circular mean exists, the normal mean will be returned. + + Args: + angles: A list of angles in radians. + """ + avg_x = sum(math.cos(ang) for ang in angles) / len(angles) + avg_y = sum(math.sin(ang) for ang in angles) / len(angles) + if (avg_x, avg_y) == (0, 0): # just return the normal mean + return sum(angles) / len(angles) + return math.atan2(avg_y, avg_x)
+ + + def _cast_to_float(self, value): + """Ensure that an input coordinate value is a float.""" + try: + number = float(value) + except Exception: + raise TypeError( + 'Coordinates must be numbers. Got {}: {}.'.format(type(value), value)) + return number + + @staticmethod + def _rotate(vec, angle): + """Hidden rotation method used by both Point2D and Vector2D.""" + cos_a = math.cos(angle) + sin_a = math.sin(angle) + qx = cos_a * vec.x - sin_a * vec.y + qy = sin_a * vec.x + cos_a * vec.y + return Vector2D(qx, qy) + + @staticmethod + def _reflect(vec, normal): + """Hidden reflection method used by both Point2D and Vector2D.""" + d = 2 * (vec.x * normal.x + vec.y * normal.y) + return Vector2D(vec.x - d * normal.x, vec.y - d * normal.y) + + def __copy__(self): + return self.__class__(self.x, self.y) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self.x, self.y) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, (Vector2D, Point2D)) and \ + self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __nonzero__(self): + return self.x != 0 or self.y != 0 + + def __len__(self): + return 2 + + def __getitem__(self, key): + return (self.x, self.y)[key] + + def __iter__(self): + return iter((self.x, self.y)) + + def __add__(self, other): + # Vector + Point -> Point + # Vector + Vector -> Vector + if isinstance(other, Point2D): + return Point2D(self.x + other.x, self.y + other.y) + elif isinstance(other, Vector2D): + return Vector2D(self.x + other.x, self.y + other.y) + else: + raise TypeError('Cannot add {} and {}'.format( + self.__class__.__name__, type(other))) + + __radd__ = __add__ + + def __sub__(self, other): + # Vector - Point -> Point + # Vector - Vector -> Vector + if isinstance(other, Point2D): + return Point2D(self.x - other.x, self.y - other.y) + elif isinstance(other, Vector2D): + return Vector2D(self.x - other.x, self.y - other.y) + else: + raise TypeError('Cannot subtract {} and {}'.format( + self.__class__.__name__, type(other))) + + def __rsub__(self, other): + if isinstance(other, (Vector2D, Point2D)): + return Vector2D(other.x - self.x, other.y - self.y) + else: + assert hasattr(other, '__len__') and len(other) == 2, \ + 'Cannot subtract {} and {}'.format( + self.__class__.__name__, type(other)) + return Vector2D(other.x - self[0], other.y - self[1]) + + def __mul__(self, other): + assert type(other) in (int, float), \ + 'Cannot multiply types {} and {}'.format( + self.__class__.__name__, type(other)) + return Vector2D(self.x * other, self.y * other) + + __rmul__ = __mul__ + + def __div__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector2D(self.x / other, self.y / other) + + def __rdiv__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector2D(other / self.x, other / self.y) + + def __floordiv__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector2D(operator.floordiv(self.x, other), + operator.floordiv(self.y, other)) + + def __rfloordiv__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector2D(operator.floordiv(other, self.x), + operator.floordiv(other, self.y)) + + def __truediv__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector2D(operator.truediv(self.x, other), + operator.truediv(self.y, other)) + + def __rtruediv__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector2D(operator.truediv(other, self.x), + operator.truediv(other, self.y)) + + def __neg__(self): + return Vector2D(-self.x, -self.y) + + __pos__ = __copy__ + + def __abs__(self): + return math.sqrt(self.x ** 2 + self.y ** 2) + +
+[docs] + def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__()
+ + + def __repr__(self): + """Vector2D representation.""" + return 'Vector2D (%.2f, %.2f)' % (self.x, self.y)
+ + + +
+[docs] +class Point2D(Vector2D): + """2D Point object. + + Args: + x: Number for the X coordinate. + y: Number for the Y coordinate. + + Properties: + * x + * y + """ + __slots__ = () + + @property + def min(self): + """Always equal to the point itself. + + This property exists to help with bounding box calculations. + """ + return self + + @property + def max(self): + """Always equal to the point itself. + + This property exists to help with bounding box calculations. + """ + return self + +
+[docs] + def move(self, moving_vec): + """Get a point that has been moved along a vector. + + Args: + moving_vec: A Vector2D with the direction and distance to move the point. + """ + return Point2D(self.x + moving_vec.x, self.y + moving_vec.y)
+ + +
+[docs] + def rotate(self, angle, origin): + """Rotate a point counterclockwise by a certain angle around an origin. + + Args: + angle: An angle for rotation in radians. + origin: A Point2D for the origin around which the point will be rotated. + """ + return Vector2D._rotate(self - origin, angle) + origin
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a point reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector2D representing the normal vector for the plane across + which the point will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point2D representing the origin from which to reflect. + """ + return Vector2D._reflect(self - origin, normal) + origin
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a point by a factor from an origin point. + + Args: + factor: A number representing how much the point should be scaled. + origin: A Point2D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0). + """ + if origin is None: + return Point2D(self.x * factor, self.y * factor) + else: + return (factor * (self - origin)) + origin
+ + +
+[docs] + def distance_to_point(self, point): + """Get the distance from this point to another Point2D.""" + vec = (self.x - point.x, self.y - point.y) + return math.sqrt(vec[0] ** 2 + vec[1] ** 2)
+ + +
+[docs] + def to_dict(self): + """Get Point2D as a dictionary.""" + return {'type': 'Point2D', + 'x': self.x, + 'y': self.y}
+ + + def __add__(self, other): + # Point + Vector -> Point + # Point + Point -> Vector + if isinstance(other, Point2D): + return Vector2D(self.x + other.x, self.y + other.y) + elif isinstance(other, Vector2D): + return Point2D(self.x + other.x, self.y + other.y) + else: + raise TypeError('Cannot add Point2D and {}'.format(type(other))) + + def __sub__(self, other): + # Point - Vector -> Point + # Point - Point -> Vector + if isinstance(other, Point2D): + return Vector2D(self.x - other.x, self.y - other.y) + elif isinstance(other, Vector2D): + return Point2D(self.x - other.x, self.y - other.y) + else: + raise TypeError('Cannot subtract Point2D and {}'.format(type(other))) + + def __repr__(self): + """Point2D representation.""" + return 'Point2D (%.2f, %.2f)' % (self.x, self.y) + + def __lt__(self, other): + """ Lesser then inequality method. This is used by certain external + data structure libraries to efficiently store and retrieve point data. + """ + if isinstance(other, Vector2D): + return self.x < other.x + + def __gt__(self, other): + """ Greater then inequality method. This is used by certain external + data structure libraries to efficiently store and retrieve point data. + """ + if isinstance(other, Vector2D): + return self.x > other.x
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry2d/polygon.html b/docs/_modules/ladybug_geometry/geometry2d/polygon.html new file mode 100644 index 00000000..b6bbcb1c --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry2d/polygon.html @@ -0,0 +1,4074 @@ + + + + + + + ladybug_geometry.geometry2d.polygon — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry2d.polygon

+# coding=utf-8
+"""2D Polygon"""
+from __future__ import division
+import math
+import time
+from collections import deque
+
+try:  # Python3
+    from queue import PriorityQueue
+except ImportError:  # Python2
+    from Queue import PriorityQueue
+
+from .pointvector import Point2D, Vector2D
+from .line import LineSegment2D
+from .ray import Ray2D
+from .polyline import Polyline2D
+from ..triangulation import _linked_list, _eliminate_holes
+from ..intersection2d import intersect_line2d, intersect_line2d_infinite, \
+    does_intersection_exist_line2d, closest_point2d_on_line2d, \
+    closest_end_point2d_between_line2d, closest_point2d_on_line2d_infinite
+from ._2d import Base2DIn2D
+import ladybug_geometry.boolean as pb
+
+inf = float("inf")
+
+
+
+[docs] +class Polygon2D(Base2DIn2D): + """2D polygon object. + + Args: + vertices: A list of Point2D objects representing the vertices of the polygon. + + Properties: + * vertices + * segments + * inside_angles + * outside_angles + * min + * max + * center + * perimeter + * area + * is_clockwise + * is_convex + * is_self_intersecting + * self_intersection_points + * is_valid + """ + __slots__ = ('_segments', '_inside_angles', '_outside_angles', '_perimeter', '_area', + '_is_clockwise', '_is_convex', '_is_self_intersecting') + + def __init__(self, vertices): + """Initialize Polygon2D.""" + Base2DIn2D.__init__(self, vertices) + self._segments = None + self._perimeter = None + self._inside_angles = None + self._outside_angles = None + self._area = None + self._is_clockwise = None + self._is_convex = None + self._is_self_intersecting = None + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Polygon2D from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "type": "Polygon2D", + "vertices": [(0, 0), (10, 0), (0, 10)] + } + """ + return cls(tuple(Point2D.from_array(pt) for pt in data['vertices']))
+ + +
+[docs] + @classmethod + def from_array(cls, point_array): + """Create a Polygon2D from a nested array of vertex coordinates. + + Args: + point_array: nested array of point arrays. + """ + return Polygon2D(Point2D(*point) for point in point_array)
+ + +
+[docs] + @classmethod + def from_rectangle(cls, base_point, height_vector, base, height): + """Initialize Polygon2D from rectangle parameters. + + Initializing a polygon this way has the added benefit of having its properties + quickly computed. + + Args: + base_point: A Point2D for the lower left vertex of the polygon. + height_vector: A vector denoting the direction of the rectangle height. + base: A number indicating the length of the base of the rectangle. + height: A number indicating the length of the height of the rectangle. + """ + assert isinstance(base_point, Point2D), \ + 'base_point must be Point2D. Got {}.'.format(type(base_point)) + assert isinstance(height_vector, Vector2D), \ + 'height_vector must be Vector2D. Got {}.'.format(type(height_vector)) + assert isinstance(base, (float, int)), 'base must be a number.' + assert isinstance(height, (float, int)), 'height must be a number.' + _hv_norm = height_vector.normalize() + _bv = Vector2D(_hv_norm.y, -_hv_norm.x) * base + _hv = _hv_norm * height + _verts = (base_point, base_point + _bv, base_point + _hv + _bv, base_point + _hv) + polygon = cls(_verts) + polygon._perimeter = base * 2 + height * 2 + polygon._area = base * height + polygon._is_clockwise = False + polygon._is_convex = True + polygon._is_self_intersecting = False + return polygon
+ + +
+[docs] + @classmethod + def from_regular_polygon(cls, number_of_sides, radius=1, base_point=Point2D()): + """Initialize Polygon2D from regular polygon parameters. + + Args: + number_of_sides: An integer for the number of sides on the regular + polygon. This number must be greater than 2. + radius: A number indicating the distance from the polygon's center + where the vertices of the polygon will lie. + The default is set to 1. + base_point: A Point2D for the center of the regular polygon. + The default is the Origin at (0, 0). + """ + assert isinstance(number_of_sides, int), 'number_of_sides must be an ' \ + 'integer. Got {}.'.format(type(number_of_sides)) + assert number_of_sides > 2, 'number_of_sides must be greater than 2. ' \ + 'Got {}.'.format(number_of_sides) + assert isinstance(base_point, Point2D), \ + 'base_point must be Point2D. Got {}.'.format(type(base_point)) + assert isinstance(radius, (float, int)), 'height must be a number.' + + # calculate angle at which each vertex is rotated from the previous one + angle = (math.pi * 2) / number_of_sides + cos_a = math.cos(angle) + sin_a = math.sin(angle) + + # pick a starting vertex that makes sense for the number of sides + if number_of_sides % 2 == 0: + start_vert = Point2D(base_point.x - radius, base_point.y) + start_vert = start_vert.rotate(angle / 2, base_point) + else: + start_vert = Point2D(base_point.x, base_point.y + radius) + _vertices = [start_vert] + + # generate the vertices + for i in range(number_of_sides - 1): + last_pt = _vertices[-1] + qx = cos_a * (last_pt.x - base_point.x) - sin_a * (last_pt.y - base_point.y) + qy = sin_a * (last_pt.x - base_point.x) + cos_a * (last_pt.y - base_point.y) + _vertices.append(Point2D(qx + base_point.x, qy + base_point.y)) + + # build the new polygon and set the properties that we know. + _new_poly = cls(_vertices) + _new_poly._is_clockwise = False + _new_poly._is_convex = True + _new_poly._is_self_intersecting = False + return _new_poly
+ + +
+[docs] + @classmethod + def from_shape_with_hole(cls, boundary, hole): + """Initialize a Polygon2D from a boundary shape with a hole inside of it. + + This method will convert the shape into a single concave polygon by drawing + a line from the hole to the outer boundary. + + Args: + boundary: A list of Point2D objects for the outer boundary of the polygon + inside of which the hole is contained. + hole: A list of Point2D objects for the hole. + """ + # check that the inputs are in the correct format + assert isinstance(boundary, list), \ + 'boundary should be a list. Got {}'.format(type(boundary)) + assert isinstance(hole, list), \ + 'hole should be a list. Got {}'.format(type(hole)) + + # check that the direction of vertices for the hole is opposite the boundary + bound_direction = Polygon2D._are_clockwise(boundary) + if cls._are_clockwise(hole) is bound_direction: + hole.reverse() + + # join the hole with the boundary at the closest point + dist_dict = {} + for i, b_pt in enumerate(boundary): + for j, h_pt in enumerate(hole): + dist_dict[b_pt.distance_to_point(h_pt)] = (i, j) + boundary = cls._merge_boundary_and_hole(boundary, hole, dist_dict) + + # return the polygon with some properties set based on what we know + _new_poly = cls(boundary) + _new_poly._is_clockwise = bound_direction + _new_poly._is_convex = False + return _new_poly
+ + +
+[docs] + @classmethod + def from_shape_with_holes(cls, boundary, holes): + """Initialize a Polygon2D from a boundary shape with holes inside of it. + + This method will convert the shape into a single concave polygon by drawing + lines from the holes to the outer boundary. + + Args: + boundary: A list of Point2D objects for the outer boundary of the polygon + inside of which all of the holes are contained. + holes: A list of lists with one list for each hole in the shape. Each hole + should be a list of at least 3 Point2D objects. + """ + # check that the inputs are in the correct format. + assert isinstance(boundary, list), \ + 'boundary should be a list. Got {}'.format(type(boundary)) + assert isinstance(holes, list), \ + 'holes should be a list. Got {}'.format(type(holes)) + for hole in holes: + assert isinstance(hole, list), \ + 'hole should be a list. Got {}'.format(type(hole)) + assert len(hole) >= 3, \ + 'hole should have at least 3 vertices. Got {}'.format(len(hole)) + + # check that the direction of vertices for the hole is opposite the boundary + bound_direction = cls._are_clockwise(boundary) + for hole in holes: + if cls._are_clockwise(hole) is bound_direction: + hole.reverse() + + # recursively add the nearest hole to the boundary until there are none left. + boundary = cls._merge_boundary_and_holes(boundary, holes) + + # return the polygon with some properties set based on what we know + _new_poly = cls(boundary) + _new_poly._is_clockwise = bound_direction + _new_poly._is_convex = False + return _new_poly
+ + +
+[docs] + @classmethod + def from_shape_with_holes_fast(cls, boundary, holes): + """Initialize a Polygon2D from a boundary shape with holes using a fast method. + + This method is similar in principle to the from_shape_with_holes method + but it uses David Eberly's algorithm for finding a bridge between the holes + and outer polygon. This is extremely fast in comparison to the methods used + by from_shape_with_holes but is not always the prettiest or the shortest + pathway through the holes. Granted, it is very practical for shapes with + lots of holes (eg. 100 holes) and will run in a fraction of the time for + this case. + + Args: + boundary: A list of Point2D objects for the outer boundary of the polygon + inside of which all of the holes are contained. + holes: A list of lists with one list for each hole in the shape. Each hole + should be a list of at least 3 Point2D objects. + """ + # check the initial direction of the boundary vertices + bound_direction = cls._are_clockwise(boundary) + # format the coordinates for input to the earcut methods + vert_coords, hole_indices = [], None + for pt in boundary: + vert_coords.append(pt.x) + vert_coords.append(pt.y) + hole_indices = [] + for hole in holes: + hole_indices.append(int(len(vert_coords) / 2)) + for pt in hole: + vert_coords.append(pt.x) + vert_coords.append(pt.y) + + # eliminate the holes within the list + outer_len = hole_indices[0] * 2 + outer_node = _linked_list(vert_coords, 0, outer_len, 2, True) + outer_node = _eliminate_holes(vert_coords, hole_indices, outer_node, 2) + + # loop through the chain of nodes and translate them to Point2D + start_i = outer_node.i + vertices = [Point2D(outer_node.x, outer_node.y)] + node = outer_node.next + node_counter, orig_start_i = 0, 0 + while node.i != start_i: + vertices.append(Point2D(node.x, node.y)) + node_counter += 1 + if node.i == 0: + orig_start_i = node_counter + node = node.next + + # ensure that the starting vertex is the same as the input boundary + vertices = vertices[orig_start_i:] + vertices[:orig_start_i] + vertices[0] = boundary[0] # this avoids issues of floating point tolerance + + # return the polygon with some properties set based on what we know + _new_poly = cls(vertices) + _new_poly._is_clockwise = bound_direction + _new_poly._is_convex = False + _new_poly._is_self_intersecting = False + return _new_poly
+ + + @property + def vertices(self): + """Tuple of all vertices in this geometry.""" + return self._vertices + + @property + def segments(self): + """Tuple of all line segments in the polygon.""" + if self._segments is None: + _segs = self._segments_from_vertices(self.vertices) + self._segments = tuple(_segs) + return self._segments + + @property + def inside_angles(self): + """Tuple of angles in radians for the interior angles of the polygon. + + These are aligned with the vertices such that the first angle corresponds + to the inside angle at the first vertex, the second at the second vertex, + and so on. + """ + if self._inside_angles is None: + angles, is_clock = [], self.is_clockwise + for i, pt in enumerate(self.vertices): + v1 = self.vertices[i - 2] - self.vertices[i - 1] + v2 = pt - self.vertices[i - 1] + v_angle = v1.angle_counterclockwise(v2) if is_clock \ + else v1.angle_clockwise(v2) + angles.append(v_angle) + angles.append(angles.pop(0)) # move the start angle to the end + self._inside_angles = tuple(angles) + return self._inside_angles + + @property + def outside_angles(self): + """Tuple of angles in radians for the exterior angles of the polygon. + + These are aligned with the vertices such that the first angle corresponds + to the inside angle at the first vertex, the second at the second vertex, + and so on. + """ + if self._outside_angles is None: + pi2 = math.pi * 2 + self._outside_angles = tuple(pi2 - a for a in self.inside_angles) + return self._outside_angles + + @property + def perimeter(self): + """The perimeter of the polygon.""" + if self._perimeter is None: + self._perimeter = sum([seg.length for seg in self.segments]) + return self._perimeter + + @property + def area(self): + """The area of the polygon.""" + if self._area is None: + _a = 0 + for i, pt in enumerate(self.vertices): + _a += self.vertices[i - 1].x * pt.y - self.vertices[i - 1].y * pt.x + self._area = _a / 2 + return abs(self._area) + + @property + def is_clockwise(self): + """Boolean for whether the polygon vertices are in clockwise order.""" + if self._is_clockwise is None: + if self._area is None: + self.area + self._is_clockwise = self._area < 0 + return self._is_clockwise + + @property + def is_convex(self): + """Boolean noting whether the polygon is convex (True) or non-convex (False).""" + if self._is_convex is None: + self._is_convex = True + if len(self.vertices) == 3: + pass + else: + _segs = self.segments + if self.is_clockwise: + for i, _s in enumerate(_segs): + if _segs[i - 1].v.determinant(_s.v) > 0: # counterclockwise turn + self._is_convex = False + break + else: + for i, _s in enumerate(_segs): + if _segs[i - 1].v.determinant(_s.v) < 0: # clockwise turn + self._is_convex = False + break + return self._is_convex + + @property + def is_self_intersecting(self): + """Boolean noting whether the polygon has self-intersecting edges. + + Note that this property is relatively computationally intense to obtain compared + to properties like area and is_convex. Also, most CAD programs forbid geometry + with self-intersecting edges. So it is recommended that this property only + be used in quality control scripts where the origin of the geometry is unknown. + """ + if self._is_self_intersecting is None: + self._is_self_intersecting = False + _segs = self.segments + for i, _s in enumerate(_segs[1: len(_segs) - 1]): + _skip = (i, i + 1, i + 2) + _other_segs = [x for j, x in enumerate(_segs) if j not in _skip] + for _oth_s in _other_segs: + if _s.intersect_line_ray(_oth_s) is not None: # intersection! + self._is_self_intersecting = True + break + if self._is_self_intersecting is True: + break + return self._is_self_intersecting + + @property + def self_intersection_points(self): + """A tuple of Point2Ds for the locations where the polygon intersects itself. + + This will be an empty tuple if the polygon is not self-intersecting and it + is generally recommended that the Polygon2D.is_self_intersecting property + be checked before using this property. + """ + if self.is_self_intersecting: + int_pts = [] + _segs = self.segments + for i, _s in enumerate(_segs[1: len(_segs) - 1]): + _skip = (i, i + 1, i + 2) + _other_segs = [x for j, x in enumerate(_segs) if j not in _skip] + for _oth_s in _other_segs: + int_pt = _s.intersect_line_ray(_oth_s) + if int_pt is not None: # intersection! + int_pts.append(int_pt) + return tuple(int_pts) + return () + + @property + def is_valid(self): + """Boolean noting whether the polygon is valid (having a non-zero area). + + Note that polygons are still considered valid if they have self-intersecting + edges, or duplicate/colinear vertices. The s_self_intersecting property + identifies self-intersecting edges, and the remove_colinear_vertices method + will remove duplicate/colinear vertices. + """ + return not self.area == 0 + +
+[docs] + def is_equivalent(self, other, tolerance): + """Boolean for equivalence between this polygon and another (within tolerance). + + The order of the polygon vertices do not have to start from the + same vertex for equivalence to be true, but must be in the same counterclockwise + or clockwise order. + + Args: + other: Polygon2D for comparison. + tolerance: float representing point equivalence. + + Returns: + True if equivalent else False + """ + + # Check number of points + if len(self.vertices) != len(other.vertices): + return False + + vertices = self.vertices + + # Check order + if not vertices[0].is_equivalent(other.vertices[0], tolerance): + self_idx = None + other_pt = other.vertices[0] + for i, pt in enumerate(self.vertices): + if pt.is_equivalent(other_pt, tolerance): + self_idx = i + break + + if self_idx is None: + return False + + # Re-order polygon vertices to match other + vertices = vertices[self_idx:] + vertices[:self_idx] + + is_equivalent = True + for pt, other_pt in zip(vertices[1:], other.vertices[1:]): + is_equivalent = is_equivalent and pt.is_equivalent(other_pt, tolerance) + return is_equivalent
+ + +
+[docs] + def is_rectangle(self, angle_tolerance): + """Test whether this Polygon2D is a rectangle given an angle tolerance. + + Note that this method will return False if the Polygon2D does not have + four vertices, even if they are duplicated or colinear. + + Args: + angle_tolerance: The max angle in radians that the corners of the + rectangle can differ from a right angle before it is not + considered a rectangle. + """ + if len(self.vertices) != 4: + return False + min_ang = (math.pi / 2) - angle_tolerance + max_ang = (math.pi / 2) + angle_tolerance + for i, pt in enumerate(self.vertices): + v1 = self.vertices[i - 2] - self.vertices[i - 1] + v2 = pt - self.vertices[i - 1] + v_angle = v1.angle(v2) + if v_angle < min_ang or v_angle > max_ang: + return False + return True
+ + +
+[docs] + def rectangular_approximation(self): + """Get a rectangular Polygon2D with the same area and aspect ratio as this one. + + This is useful when an interface requires a rectangular input but the + user-defined geometry can be any shape. The resulting rectangle will share + the same center as this one. + """ + b_rect_len = self.max.x - self.min.x + b_rect_hgt = self.max.y - self.min.y + aspect_ratio = b_rect_len / b_rect_hgt + final_hgt = math.sqrt(self.area / aspect_ratio) + final_len = self.area / final_hgt + b_pt = Point2D(self.center.x - (final_len / 2), self.center.y - (final_hgt / 2)) + return Polygon2D.from_rectangle(b_pt, Vector2D(0, 1), final_len, final_hgt)
+ + +
+[docs] + def pole_of_inaccessibility(self, tolerance): + """Get the pole of inaccessibility for the polygon. + + The pole of inaccessibility is the most distant internal point from the + polygon outline. It is not to be confused with the centroid, which + represents the "center of mass" of the polygon and may be outside of + the polygon if the shape is concave. The poly of inaccessibility is + useful for optimal placement of a text label on a polygon. + + The algorithm here is a port of the polylabel library from MapBox + assembled by Michal Hatak. (https://github.com/Twista/python-polylabel). + + Args: + tolerance: The precision to which the pole of inaccessibility + will be computed. + """ + # compute the cell size from the bounding rectangle + min_x, min_y = self.min.x, self.min.y + max_x, max_y = self.max.x, self.max.y + width = max_x - min_x + height = max_y - min_y + cell_size = min(width, height) + h = cell_size / 2.0 + max_dim = max(width, height) + if cell_size == 0 or self.area < max_dim * tolerance: + # degenerate polygon; just return the center + return self.center + + # get an array representation of the polygon and set up the priority queue + _polygon = tuple(pt.to_array() for pt in self.vertices) + cell_queue = PriorityQueue() + + # cover polygon with initial cells + x = min_x + while x < max_x: + y = min_y + while y < max_y: + c = _Cell(x + h, y + h, h, _polygon) + y += cell_size + cell_queue.put((-c.max, time.time(), c)) + x += cell_size + + best_cell = self._get_centroid_cell(_polygon) + + bbox_cell = _Cell(min_x + width / 2, min_y + height / 2, 0, _polygon) + if bbox_cell.d > best_cell.d: + best_cell = bbox_cell + + # recursively iterate until we find the pole + num_of_probes = cell_queue.qsize() + while not cell_queue.empty(): + _, __, cell = cell_queue.get() + + if cell.d > best_cell.d: + best_cell = cell + + if cell.max - best_cell.d <= tolerance: + continue + + h = cell.h / 2 + c = _Cell(cell.x - h, cell.y - h, h, _polygon) + cell_queue.put((-c.max, time.time(), c)) + c = _Cell(cell.x + h, cell.y - h, h, _polygon) + cell_queue.put((-c.max, time.time(), c)) + c = _Cell(cell.x - h, cell.y + h, h, _polygon) + cell_queue.put((-c.max, time.time(), c)) + c = _Cell(cell.x + h, cell.y + h, h, _polygon) + cell_queue.put((-c.max, time.time(), c)) + num_of_probes += 4 + return Point2D(best_cell.x, best_cell.y)
+ + +
+[docs] + def remove_duplicate_vertices(self, tolerance): + """Get a version of this polygon without duplicate vertices. + + Args: + tolerance: The minimum distance between a two vertices at which + they are considered co-located or duplicated. + """ + new_vertices = tuple( + pt for i, pt in enumerate(self._vertices) + if not pt.is_equivalent(self._vertices[i - 1], tolerance)) + return Polygon2D(new_vertices)
+ + +
+[docs] + def remove_colinear_vertices(self, tolerance): + """Get a version of this polygon without colinear or duplicate vertices. + + Args: + tolerance: The minimum distance that a vertex can be from a line + before it is considered colinear. + """ + new_vertices = [] # list to hold the new vertices + skip = 0 # track the number of vertices being skipped/removed + first_skip, is_first, = 0, True # track the number skipped from first vertex + # loop through vertices and remove all cases of colinear verts + for i, _v in enumerate(self.vertices): + _v2, _v1 = self[i - 2 - skip], self[i - 1] + _a = _v2.determinant(_v1) + _v1.determinant(_v) + _v.determinant(_v2) + b_dist = _v.distance_to_point(_v2) + b_dist = tolerance if b_dist < tolerance else b_dist + tri_tol = (b_dist * tolerance) / 2 # area of triangle with tolerance height + if abs(_a) >= tri_tol: # triangle area > tolerance; not colinear + new_vertices.append(self[i - 1]) + skip = 0 + if is_first: + is_first = False + first_skip = i - 1 + else: # colinear point to be removed + skip += 1 + # catch case of last few vertices being equal but distinct from first point + if skip != 0 and first_skip != -1: + assert abs(-2 - skip) <= len(self), \ + 'There must be at least 3 vertices for a Polygon2D.' + _v2, _v1, _v = self[-2 - skip], self[-1], self[first_skip] + _a = _v2.determinant(_v1) + _v1.determinant(_v) + _v.determinant(_v2) + b_dist = _v.distance_to_point(_v2) + b_dist = tolerance if b_dist < tolerance else b_dist + tri_tol = (b_dist * tolerance) / 2 # area of triangle with tolerance height + if abs(_a) >= tri_tol: # triangle area > area tolerance; not colinear + new_vertices.append(_v1) + return Polygon2D(new_vertices)
+ + +
+[docs] + def split_through_self_intersection(self, tolerance): + """Get a list of non-intersecting Polygon2D if this polygon intersects itself. + + If the Polygon2D does not intersect itself, then a list with the current + Polygon2D will be returned. + + Args: + tolerance: The minimum difference between vertices before they are + considered co-located. + """ + # loop over the segments and group the vertices by intersection points + intersect_groups = [[]] + _segs = self.segments + seg_count = len(_segs) + for i, _s in enumerate(_segs): + # loop over the other segments and find any intersection points + if i == 0: + _skip = (len(_segs) - 1, i, i + 1) + elif i == seg_count - 1: + _skip = (i - 1, i, 0) + else: + _skip = (i - 1, i, i + 1) + _other_segs = [x for j, x in enumerate(_segs) if j not in _skip] + int_pts = [] + for _oth_s in _other_segs: + int_pt = _s.intersect_line_ray(_oth_s) + if int_pt is not None: # intersection! + int_pts.append(int_pt) + # if intersection points were found, adjust the groups accordingly + if len(int_pts) == 0: # no self intersection on this segment + intersect_groups[-1].append(_s.p2) + elif len(int_pts) == 1: # typical self-intersection case we should split + intersect_groups[-1].append(int_pts[0]) + intersect_groups.append([_s.p2]) + else: # rare case of multiple intersections on the same segment + # sort the intersection points along the segment + dists = [_s.p1.distance_to_point(ipt) for ipt in int_pts] + sort_pts = [pt for _, pt in sorted(zip(dists, int_pts), + key=lambda pair: pair[0])] + intersect_groups[-1].append(sort_pts[0]) + for s_pt in sort_pts[1:]: + intersect_groups.append([s_pt]) + intersect_groups.append([_s.p2]) + + # process the intersect groups into polygon objects + if len(intersect_groups) == 1: + return [self] # not a self-intersecting shape + split_polygons = [] + poly_count = int(len(intersect_groups) / 2) + if len(intersect_groups[poly_count]) == 1: # rare case of start at intersect + for i in range(poly_count): + vert_group = [intersect_groups[i], intersect_groups[-i - 1]] + for verts_list in vert_group: + if len(verts_list) > 2: + try: + clean_poly = Polygon2D(verts_list) + clean_poly = clean_poly.remove_duplicate_vertices(tolerance) + split_polygons.append(clean_poly) + except AssertionError: # degenerate polygon that should not be added + pass + else: # typical case of intersection in the middle + for i in range(poly_count): + verts_list = intersect_groups[i] + intersect_groups[-i - 1] + if len(verts_list) > 2: + try: + clean_poly = Polygon2D(verts_list) + clean_poly = clean_poly.remove_duplicate_vertices(tolerance) + split_polygons.append(clean_poly) + except AssertionError: # degenerate polygon that should not be added + pass + final_verts = intersect_groups[i + 1] + try: + clean_poly = Polygon2D(final_verts) + clean_poly = clean_poly.remove_duplicate_vertices(tolerance) + split_polygons.append(clean_poly) + except AssertionError: # degenerate polygon that should not be added + pass + return split_polygons
+ + +
+[docs] + def reverse(self): + """Get a copy of this polygon where the vertices are reversed.""" + _new_poly = Polygon2D(tuple(pt for pt in reversed(self.vertices))) + self._transfer_properties(_new_poly) + if self._is_clockwise is not None: + _new_poly._is_clockwise = not self._is_clockwise + return _new_poly
+ + +
+[docs] + def move(self, moving_vec): + """Get a polygon that has been moved along a vector. + + Args: + moving_vec: A Vector2D with the direction and distance to move the polygon. + """ + _new_poly = Polygon2D(tuple(pt.move(moving_vec) for pt in self.vertices)) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def rotate(self, angle, origin): + """Get a polygon that is rotated counterclockwise by a certain angle. + + Args: + angle: An angle for rotation in radians. + origin: A Point2D for the origin around which the point will be rotated. + """ + _new_poly = Polygon2D(tuple(pt.rotate(angle, origin) for pt in self.vertices)) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a polygon reflected across a plane with the input normal and origin. + + Args: + normal: A Vector2D representing the normal vector for the plane across + which the polygon will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point2D representing the origin from which to reflect. + """ + _new_poly = Polygon2D(tuple(pt.reflect(normal, origin) for pt in self.vertices)) + self._transfer_properties(_new_poly) + if self._is_clockwise is not None: + _new_poly._is_clockwise = not self._is_clockwise + return _new_poly
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a polygon by a factor from an origin point. + + Args: + factor: A number representing how much the polygon should be scaled. + origin: A Point2D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0). + """ + if origin is None: + return Polygon2D(tuple( + Point2D(pt.x * factor, pt.y * factor) for pt in self.vertices)) + else: + return Polygon2D(tuple(pt.scale(factor, origin) for pt in self.vertices))
+ + +
+[docs] + def offset(self, distance, check_intersection=False): + """Offset the polygon by a given distance inwards or outwards. + + Note that the resulting shape may be self-intersecting if the distance + is large enough and the is_self_intersecting property may be used to identify + these shapes. + + Args: + distance: The distance inwards that the polygon will be offset. + Positive values will always be offset inwards while negative ones + will be offset outwards. + check_intersection: A boolean to note whether the resulting operation + should be checked for self intersection and, if so, None will be + returned instead of the self-intersecting polygon. + """ + # make sure the offset is not zero + if distance == 0: + return self + + # loop through the vertices and get the new offset vectors + base_verts = self._vertices if not self.is_clockwise \ + else list(reversed(self._vertices)) + init_verts = [pt for i, pt in enumerate(base_verts) if pt != base_verts[i - 1]] + if len(init_verts) < 3: # degenerate polygon + return self # cannot be offset into a valid shape + move_vecs, max_i = [], len(init_verts) - 1 + for i, pt in enumerate(init_verts): + v1 = init_verts[i - 1] - pt + end_i = i + 1 if i != max_i else 0 + v2 = init_verts[end_i] - pt + if not self.is_clockwise: + ang = v1.angle_clockwise(v2) / 2 + if ang == 0: + ang = math.pi / 2 + m_vec = v1.rotate(-ang).normalize() + m_dist = distance / math.sin(ang) + else: + ang = v1.angle_counterclockwise(v2) / 2 + if ang == 0: + ang = math.pi / 2 + m_vec = v1.rotate(ang).normalize() + m_dist = -distance / math.sin(ang) + m_vec = m_vec * m_dist + move_vecs.append(m_vec) + + # move the vertices by the offset to create the new Polygon2D + new_pts = tuple(pt.move(m_vec) for pt, m_vec in zip(init_verts, move_vecs)) + if self.is_clockwise: + new_pts = tuple(reversed(new_pts)) + new_poly = Polygon2D(new_pts) + + # check for self intersection between the moving vectors if requested + if check_intersection: + poly_segs = new_poly.segments if not self.is_clockwise else \ + Polygon2D(tuple(reversed(new_pts))).segments + _segs = [LineSegment2D(p, v) for p, v in zip(init_verts, move_vecs)] + _skip = (0, len(_segs) - 1) + _other_segs = [x for j, x in enumerate(poly_segs) if j not in _skip] + for _oth_s in _other_segs: + if _segs[0].intersect_line_ray(_oth_s) is not None: # intersection! + return None + for i, _s in enumerate(_segs[1:]): + _skip = (i, i + 1) + _other_segs = [x for j, x in enumerate(poly_segs) if j not in _skip] + for _oth_s in _other_segs: + if _s.intersect_line_ray(_oth_s) is not None: # intersection! + return None + return new_poly
+ + +
+[docs] + def intersect_line_ray(self, line_ray): + """Get the intersections between this polygon and a Ray2D or LineSegment2D. + + Args: + line_ray: A LineSegment2D or Ray2D or to intersect. + + Returns: + A list with Point2D objects for the intersections. + List will be empty if no intersection exists. + """ + intersections = [] + for _s in self.segments: + inters = intersect_line2d(_s, line_ray) + if inters is not None: + intersections.append(inters) + return intersections
+ + +
+[docs] + def intersect_line_infinite(self, ray): + """Get the intersections between this polygon and a Ray2D extended infinitely. + + Args: + ray: A Ray2D or to intersect. This will be extended in both + directions infinitely for the intersection. + + Returns: + A list with Point2D objects for the intersections. + List will be empty if no intersection exists. + """ + intersections = [] + for _s in self.segments: + inters = intersect_line2d_infinite(_s, ray) + if inters is not None: + intersections.append(inters) + return intersections
+ + +
+[docs] + def point_relationship(self, point, tolerance): + """Test whether a Point2D lies inside, outside or on the boundary of the polygon. + + Compared to other methods like is_point_inside this method is slow. However, + it covers all edge cases, including the literal edge of the polygon. + + Args: + point: A Point2D for which the relationship to the polygon will be tested. + tolerance: The minimum distance from the edge at which a point is + considered to lie on the edge. + + Returns: + An integer denoting the relationship of the point. + + This will be one of the following: + + * -1 = Outside polygon + * 0 = On the edge of the polygon + * +1 = Inside polygon + """ + if self.is_point_on_edge(point, tolerance): + return 0 + if self.is_point_inside_bound_rect(point): + return 1 + return -1
+ + +
+[docs] + def is_point_on_edge(self, point, tolerance): + """Test whether a Point2D lies on the boundary edges of the polygon. + + Args: + point: A Point2D for which the edge relationship will be tested. + tolerance: The minimum distance from the edge at which a point is + considered to lie on the edge. + + Returns: + A boolean denoting whether the point lies on the polygon edges (True) + or not on the edges (False). + """ + for _s in self.segments: + close_pt = closest_point2d_on_line2d(point, _s) + if point.distance_to_point(close_pt) <= tolerance: + return True + return False
+ + +
+[docs] + def is_point_inside_check(self, point): + """Test whether a Point2D lies inside the polygon with checks for fringe cases. + + This method uses the same calculation as the the `is_point_inside` method + but it includes additional checks for the fringe cases noted in the + `is_point_inside` description. Using this method means that it will always + yield the right result for all convex polygons and concave polygons with + one concave turn (provided that they do not have colinear vertices). + This is suitable for nearly all practical purposes and the only cases + that could yield an incorrect result are when a point is co-linear with + two or more polygon edges along the X vector like so: + + .. code-block:: shell + + _____ _____ _____ + | . |___| |___| | + |_________________________| + + While this method covers most fringe cases, it will not test for whether + a point lies perfectly on the edge of the polygon so it assesses whether + a point lies inside the polygon up to Python floating point tolerance + (16 digits). If distinguishing edge conditions from inside/ outside is + important, the `point_relationship` method should be used. + + Args: + point: A Point2D for which the inside/ outside relationship will be tested. + Returns: + A boolean denoting whether the point lies inside (True) or outside (False). + """ + def non_duplicate_intersect(test_ray): + inters = [] + n_int = 0 + for _s in self.segments: + inter = intersect_line2d(_s, test_ray) + if inter is not None: + try: + if inter != inters[-1]: # ensure intersection is not duplicated + n_int += 1 + inters.append(inter) + except IndexError: + n_int += 1 + inters.append(inter) + return n_int, inters + + n_int, inters = non_duplicate_intersect(Ray2D(point, Vector2D(1, 0))) + # check that intersections do not form a polygon segment co-linear with test_ray + if self.is_convex is False and n_int == 2: + for _s in self.segments: + if _s.p1 == inters[0] and _s.p2 == inters[1]: + return self.is_point_inside(point, Vector2D(0, 1)) + if n_int % 2 == 0: + return False + + n_int, inters = non_duplicate_intersect(Ray2D(point, Vector2D(0, 1))) + # check that intersections do not form a polygon segment co-linear with test_ray + if self.is_convex is False and n_int == 2: + for _s in self.segments: + if _s.p1 == inters[0] and _s.p2 == inters[1]: + return self.is_point_inside(point, Vector2D(1, 0)) + if n_int % 2 == 0: + return False + return True
+ + +
+[docs] + def is_point_inside(self, point, test_vector=Vector2D(1, 0.00001)): + """Test whether a Point2D lies inside or outside the polygon. + + This method is the fastest way to tell if a point is inside a polygon when + the given point lies inside the boundary rectangle of the polygon. + However, while this method gives the correct result in 99.9% of cases, + there are a few fringe cases where it will not give the correct result. + Specifically these are: + + .. code-block:: shell + + 1 - When the test_ray intersects perfectly with a polygon vertex. + For example, this case with an X-unit (1, 0) test_vector: + _____________ + | . | + | / + |___________/ + 2 - When there are two polygon vertices that are colinear with the point + along the test_ray. For example, this case with an X-unit test_vector: + _____ + | . |____ + |__________| + + Use the `is_point_inside_check` method if a result that covers these fringe + cases is needed. Oftentimes, it is more practical to use a test_vector + with a low probability of encountering the fringe cases than to use the + (much longer) `is_point_inside_check` method. + + Args: + point: A Point2D for which the inside/outside relationship will be tested. + test_vector: Optional vector to set the direction in which intersections + with the polygon edges will be evaluated to determine if the + point is inside. Default is a slight variation of the X-unit + vector with a low probability of encountering the unsupported + fringe cases. + + Returns: + A boolean denoting whether the point lies inside (True) or outside (False). + """ + test_ray = Ray2D(point, test_vector) + n_int = 0 + for _s in self.segments: + if does_intersection_exist_line2d(_s, test_ray): + n_int += 1 + if n_int % 2 == 0: + return False + return True
+ + +
+[docs] + def is_point_inside_bound_rect(self, point, test_vector=Vector2D(1, 0.00001)): + """Test whether a Point2D lies roughly inside or outside the polygon. + + This function is virtually identical to the `is_point_inside` + method but will first do a check to see if the point lies inside the + polygon bounding rectangle. As such, it is a faster approach when one + expects many of tested points to lie far away from the polygon. + + Args: + point: A Point2D for which the inside/ outside relationship will be tested. + test_vector: Optional vector to set the direction in which intersections + with the polygon edges will be evaluated to determine if the + point is inside. Default is the X unit vector. + + Returns: + A boolean denoting whether the point lies inside (True) or outside (False). + """ + min = self.min + max = self.max + if point.x < min.x or point.y < min.y or point.x > max.x or point.y > max.y: + return False + return self.is_point_inside(point, test_vector)
+ + +
+[docs] + def is_polygon_inside(self, polygon): + """Test whether another Polygon2D lies completely inside this polygon. + + Args: + polygon: A Polygon2D to test whether it is completely inside this one. + + Returns: + A boolean denoting whether the polygon lies inside (True) or not (False). + """ + # if the first polygon vertex lies outside, we know it is not inside. + if not self.is_point_inside_bound_rect(polygon[0]): + return False + # if one of the edges intersects, we know it has crossed outside. + for seg in self.segments: + for _s in polygon.segments: + if does_intersection_exist_line2d(seg, _s): + return False + return True
+ + +
+[docs] + def is_polygon_outside(self, polygon): + """Test whether another Polygon2D lies completely outside this polygon. + + Args: + polygon: A Polygon2D to test whether it is completely outside this one. + + Returns: + A boolean denoting whether the polygon lies outside (True) or not (False). + """ + # if the first polygon vertex lies inside, we know it is not outside. + if self.is_point_inside_bound_rect(polygon[0]): + return False + # if one of the edges intersects, we know it has crossed inside. + for seg in self.segments: + for _s in polygon.segments: + if does_intersection_exist_line2d(seg, _s): + return False + return True
+ + +
+[docs] + def does_polygon_touch(self, polygon, tolerance): + """Test whether another Polygon2D touches, overlaps or is inside this polygon. + + Args: + polygon: A Polygon2D to test whether it touches this polygon. + tolerance: The minimum distance from an edge at which a point is + considered to touch the edge. + + Returns: + A boolean denoting whether the polygon touches (True) or not (False). + """ + # perform a bounding rectangle check to see if the polygons cannot overlap + if not Polygon2D.overlapping_bounding_rect(self, polygon, tolerance): + return False + + # first evaluate the point relationships + pt_rels1 = [self.point_relationship(pt, tolerance) for pt in polygon] + if 0 in pt_rels1 or 1 in pt_rels1: + return True # definitely touching polygons + pt_rels2 = [polygon.point_relationship(pt, tolerance) for pt in self] + if 0 in pt_rels2 or 1 in pt_rels2: + return True # definitely touching polygons + + # if any of the segments intersect the other polygon, there is overlap + for seg in self.segments: + for _s in polygon.segments: + if does_intersection_exist_line2d(seg, _s): + return True + + # we can reliably say that the polygons do not touch + return False
+ + +
+[docs] + def polygon_relationship(self, polygon, tolerance): + """Test whether another Polygon2D lies inside, outside or overlaps this one. + + This method is not usually the fastest for understanding the relationship + between polygons but it accurately accounts for tolerance such that the + case of the two polygons sharing edges will not determine the outcome. + Only when the polygon has vertices that are truly outside this polygon + within the tolerance will the relationship become outside (or intersecting + if one of the vertices is already inside within the tolerance). + + In the case of the input polygon being identical to the current polygon, + the relationship will be Inside. + + Args: + polygon: A Polygon2D for which the relationship to the current polygon + will be tested. + tolerance: The minimum distance from the edge at which a point is + considered to lie on the edge. + + Returns: + An integer denoting the relationship of the polygon. + + This will be one of the following: + + * -1 = Outside this polygon + * 0 = Overlaps (intersects) this polygon + * +1 = Inside this polygon + """ + # perform a bounding rectangle check to see if the polygons cannot overlap + if not Polygon2D.overlapping_bounding_rect(self, polygon, tolerance): + return -1 + + # first evaluate the point relationships to rule out the inside case + pt_rels1 = [self.point_relationship(pt, tolerance) for pt in polygon] + pt_rels2 = [polygon.point_relationship(pt, tolerance) for pt in self] + if all(r1 >= 0 for r1 in pt_rels1) and all(r2 <= 0 for r2 in pt_rels2): + poi = polygon._point_in_polygon(tolerance) + if self.is_point_inside(poi) == 1: + return 1 # definitely inside the polygon + if 1 in pt_rels1 or 1 in pt_rels2: + return 0 # definitely overlap in the polygons + if all(r2 == 0 for r2 in pt_rels2): + poi = self._point_in_polygon(tolerance) + if polygon.is_point_inside(poi) == 1: + return 0 + + # offset one of the polygons inward by the tolerance + off_poly = polygon.offset(tolerance) + # if any of the offset segments intersect the other polygon, there is overlap + for seg in self.segments: + for _s in off_poly.segments: + if does_intersection_exist_line2d(seg, _s): + return 0 + + # we can reliably say that the polygons have nothing to do with one another + return -1
+ + +
+[docs] + def distance_to_point(self, point): + """Get the minimum distance between this shape and a point. + + Points that are inside the Polygon2D will return a distance of zero. + If the distance of an interior point to an edge is needed, the + distance_from_edge_to_point method should be used. + + Args: + point: A Point2D object to which the minimum distance will be computed. + + Returns: + The distance to the input point. Will be zero if the point is + inside the Polygon2D. + """ + if self.is_point_inside_bound_rect(point): + return 0 + return min(seg.distance_to_point(point) for seg in self.segments)
+ + +
+[docs] + def distance_from_edge_to_point(self, point): + """Get the minimum distance between the edge of this shape and the input point. + + Args: + point: A Point2D object to which the minimum distance will be computed. + + Returns: + The distance to the input point from the nearest edge. + """ + return min(seg.distance_to_point(point) for seg in self.segments)
+ + +
+[docs] + def snap_to_polygon(self, polygon, tolerance): + """Snap another Polygon2D to this one for differences smaller than the tolerance. + + This is useful to run before performing operations where small tolerance + differences are likely to cause issues, such as in boolean operations. + + Args: + polygon: A Polygon2D which will be snapped to the current polygon. + tolerance: The minimum distance at which points will be snapped. + + Returns: + A version of the polygon that is snapped to this Polygon2D. + """ + new_verts = [] + for pt in polygon.vertices: + # first check if the point can be snapped to a vertex + for s_pt in self.vertices: + if pt.is_equivalent(s_pt, tolerance): + new_verts.append(s_pt) + break + else: + # check if the point can be snapped to a segment + for seg in self.segments: + s_pt = seg.closest_point(pt) + if s_pt.distance_to_point(pt) <= tolerance: + new_verts.append(s_pt) + break + else: # point could not be snapped + new_verts.append(pt) + return Polygon2D(new_verts)
+ + +
+[docs] + def snap_to_grid(self, grid_increment): + """Snap this polygon's vertices to the nearest grid node defined by an increment. + + Args: + grid_increment: A positive number for dimension of each grid cell. This + typically should be equal to the tolerance or larger but should + not be larger than the smallest detail of the polygon that you + wish to resolve. + + Returns: + A version of this polygon that is snapped to the grid. + """ + new_verts = [] + for pt in self.vertices: + new_x = grid_increment * round(pt.x / grid_increment) + new_y = grid_increment * round(pt.y / grid_increment) + new_verts.append(Point2D(new_x, new_y)) + return Polygon2D(new_verts)
+ + +
+[docs] + def to_dict(self): + """Get Polygon2D as a dictionary.""" + return {'type': 'Polygon2D', + 'vertices': [pt.to_array() for pt in self.vertices]}
+ + +
+[docs] + def to_array(self): + """Get a tuple of tuples where each sub-tuple represents a Point2D vertex.""" + return tuple(pt.to_array() for pt in self.vertices)
+ + + def _to_bool_poly(self): + """Translate the Polygon2D to a BooleanPolygon.""" + b_pts = (pb.BooleanPoint(pt.x, pt.y) for pt in self.vertices) + return pb.BooleanPolygon([b_pts]) + + def _to_snapped_bool_poly(self, snap_ref_polygon, tolerance): + """Snap a Polygon2D to this one and translate it to a BooleanPolygon. + + This is necessary to ensure that boolean operations will succeed between + two polygons. + """ + new_poly = snap_ref_polygon.snap_to_polygon(self, tolerance) + return new_poly._to_bool_poly() + + @staticmethod + def _from_bool_poly(bool_polygon, tolerance=None): + """Get a list of Polygon2D from a BooleanPolygon object. + + Args: + bool_polygon: A BooleanPolygon to be interpreted to a list of Polygon2D. + tolerance: An optional tolerance value to be used to remove + degenerate objects from the result. If None, the result may + contain degenerate objects. + """ + polys = [] + for new_poly in bool_polygon.regions: + if len(new_poly) > 2: + poly = Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in new_poly)) + if tolerance is not None: + try: + poly = poly.remove_duplicate_vertices(tolerance) + polys.append(poly) + except AssertionError: + pass # degenerate polygon to be removed + else: + polys.append(poly) + return polys + +
+[docs] + def boolean_union(self, polygon, tolerance): + """Get a list of Polygon2D for the union of this Polygon and another. + + Note that the result will not differentiate hole polygons from boundary + polygons and so it may be desirable to use the Polygon2D.is_polygon_inside + method to distinguish whether a given polygon in the result represents + a hole in another polygon in the result. + + Also note that this method will return the original polygons when there + is no overlap in the two. + + Args: + polygon: A Polygon2D for which the union with the current polygon + will be computed. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A list of Polygon2D representing the union of the two polygons. + """ + result = pb.union( + self._to_bool_poly(), + polygon._to_snapped_bool_poly(self, tolerance), + tolerance / 1000 + ) + return Polygon2D._from_bool_poly(result, tolerance)
+ + +
+[docs] + def boolean_intersect(self, polygon, tolerance): + """Get a list of Polygon2D for the intersection of this Polygon and another. + + Args: + polygon: A Polygon2D for which the intersection with the current polygon + will be computed. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A list of Polygon2D representing the intersection of the two polygons. + Will be an empty list if no overlap exists between the polygons. + """ + result = pb.intersect( + self._to_bool_poly(), + polygon._to_snapped_bool_poly(self, tolerance), + tolerance / 1000 + ) + return Polygon2D._from_bool_poly(result, tolerance)
+ + +
+[docs] + def boolean_difference(self, polygon, tolerance): + """Get a list of Polygon2D for the subtraction of another polygon from this one. + + Args: + polygon: A Polygon2D for which the difference with the current polygon + will be computed. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A list of Polygon2D representing the difference of the two polygons. + Will be an empty list if subtracting the polygons results in the complete + elimination of this polygon. Will be the original polygon when there + is no overlap between the polygons. + """ + result = pb.difference( + self._to_bool_poly(), + polygon._to_snapped_bool_poly(self, tolerance), + tolerance / 1000 + ) + return Polygon2D._from_bool_poly(result, tolerance)
+ + +
+[docs] + def boolean_xor(self, polygon, tolerance): + """Get Polygon2D list for the exclusive disjunction of this polygon and another. + + Note that this method is prone to merging holes that may exist in the + result into the boundary to create a single list of joined vertices, + which may not always be desirable. In this case, it may be desirable + to do two separate boolean_difference calculations instead. + + Also note that, when the result includes separate polygons for holes, + it will not differentiate hole polygons from boundary polygons + and so it may be desirable to use the Polygon2D.is_polygon_inside + method to distinguish whether a given polygon in the result represents + a hole within another polygon in the result. + + Args: + polygon: A Polygon2D for which the exclusive disjunction with the + current polygon will be computed. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A list of Polygon2D representing the exclusive disjunction of the + two polygons. Will be the original polygons when there is no overlap + in the two. + """ + result = pb.xor( + self._to_bool_poly(), + polygon._to_snapped_bool_poly(self, tolerance), + tolerance / 1000 + ) + return Polygon2D._from_bool_poly(result, tolerance)
+ + +
+[docs] + @staticmethod + def snap_polygons(polygons, tolerance): + """Snap several Polygon2D to each other if they differ less than the tolerance. + + This is useful to run before performing operations where small tolerance + differences are likely to cause issues, such as in boolean operations. + + Args: + polygons: A list of Polygon2D, which will be snapped to each other. + tolerance: The minimum distance at which points will be snapped. + + Returns: + A list of the input polygon2D that have been snapped to one another. + """ + new_polygons = list(polygons) + for i, poly_1 in enumerate(new_polygons): + try: + for j, poly_2 in enumerate(new_polygons[i + 1:]): + new_polygons[i + j + 1] = poly_1.snap_to_polygon(poly_2, tolerance) + except IndexError: + pass # we have reached the end of the list of polygons + return new_polygons
+ + +
+[docs] + @staticmethod + def boolean_union_all(polygons, tolerance): + """Get a list of Polygon2D for the union of several Polygon2D. + + Using this method is more computationally efficient than calling the + Polygon2D.boolean_union() method multiple times as this method will + only compute the intersection of the segments once. + + Note that the result will not differentiate hole polygons from boundary + polygons and so it may be desirable to use the Polygon2D.is_polygon_inside + method to distinguish whether a given polygon in the result represents + a hole in another polygon in the result. + + Also note that this method will return the original polygons when there + is no overlap in the two. + + Args: + polygons: An array of Polygon2D for which the union will be computed. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A list of Polygon2D representing the union of all the polygons. + """ + polygons = Polygon2D.snap_polygons(polygons, tolerance) + bool_polys = [poly._to_bool_poly() for poly in polygons] + result = pb.union_all(bool_polys, tolerance / 1000) + return Polygon2D._from_bool_poly(result, tolerance)
+ + +
+[docs] + @staticmethod + def boolean_intersect_all(polygons, tolerance): + """Get a list of Polygon2D for the intersection of several Polygon2D. + + Using this method is more computationally efficient than calling the + Polygon2D.boolean_intersect() method multiple times as this method will + only compute the intersection of the segments once. + + Note that the result will not differentiate hole polygons from boundary + polygons and so it may be desirable to use the Polygon2D.is_polygon_inside + method to distinguish whether a given polygon in the result represents + a hole in another polygon in the result. + + Also note that this method will return the original polygons when there + is no overlap in the two. + + Args: + polygons: An array of Polygon2D for which the intersection will be computed. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A list of Polygon2D representing the intersection of all the polygons. + Will be an empty list if no overlap exists between the polygons. + """ + polygons = Polygon2D.snap_polygons(polygons, tolerance) + bool_polys = [poly._to_bool_poly() for poly in polygons] + result = pb.intersect_all(bool_polys, tolerance / 1000) + return Polygon2D._from_bool_poly(result, tolerance)
+ + +
+[docs] + @staticmethod + def boolean_split(polygon1, polygon2, tolerance): + """Split two Polygon2D with one another to get the intersection and difference. + + Using this method is more computationally efficient than calling the + Polygon2D.intersect() and Polygon2D.difference() methods individually as + this method will only compute the intersection of the segments once. + + Note that the result will not differentiate hole polygons from boundary + polygons and so it may be desirable to use the Polygon2D.is_polygon_inside + method to distinguish whether a given polygon in the result represents + a hole in another polygon in the result. + + Args: + polygon1: A Polygon2D for the first polygon that will be split with + the second polygon. + polygon2: A Polygon2D for the second polygon that will be split with + the first polygon. + tolerance: The minimum distance between points before they are + considered distinct from one another. + + Returns: + A tuple with three elements + + - intersection: A list of Polygon2D for the intersection of the two + input polygons. + + - poly1_difference: A list of Polygon2D for the portion of polygon1 that does + not overlap with polygon2. When combined with the intersection, this + makes a split version of polygon1. + + - poly2_difference: A list of Polygon2D for the portion of polygon2 that does + not overlap with polygon1. When combined with the intersection, this + makes a split version of polygon2. + """ + int_result, poly1_result, poly2_result = pb.split( + polygon1._to_bool_poly(), + polygon2._to_snapped_bool_poly(polygon1, tolerance), + tolerance / 1000 + ) + intersection = Polygon2D._from_bool_poly(int_result, tolerance) + poly1_difference = Polygon2D._from_bool_poly(poly1_result, tolerance) + poly2_difference = Polygon2D._from_bool_poly(poly2_result, tolerance) + return intersection, poly1_difference, poly2_difference
+ + +
+[docs] + @staticmethod + def perimeter_core_by_offset(polygon, distance, holes=None): + """Compute perimeter and core sub-polygons using a simple offset method. + + This method will only return polygons when the distance is shallow enough + that the perimeter offset does not intersect itself or turn inward on itself. + Otherwise, the method will simply return None. + + Args: + polygon: A Polygon2D to split into perimeter and core sub-polygons. + distance: Distance in model units to offset perimeter sub-polygon. + holes: A list of Polygon2D objects representing holes in the + polygon. (Default: None). + + Returns: + A tuple with two items. + + * perimeter_sub_polys -- A list of perimeter sub-polygons as Polygon2D + objects. Will be None if the offset distance is too deep. + + * core_sub_polys -- A list of core sub-polygons as Polygon2D objects. In the + event of a core sub-polygon with a hole, a list with be returned with + the first item being a boundary and successive items as hole polygons. + Will be None if the offset distance is too deep. + """ + # extract the core polygon and make sure it doesn't intersect itself + core_sub_poly = polygon.offset(distance, check_intersection=True) + if core_sub_poly is None: + return None, None + # generate the perimeter polygons + if holes is None: + perimeter_sub_polys = [] + for out_seg, in_seg in zip(polygon.segments, core_sub_poly.segments): + pts = (out_seg.p1, out_seg.p2, in_seg.p2, in_seg.p1) + perimeter_sub_polys.append(Polygon2D(pts)) + return perimeter_sub_polys, [core_sub_poly] + else: + # offset all of the holes into the shape + core_sub_polys = [core_sub_poly] + for hole in holes: + hole_sub_poly = hole.offset(-distance, check_intersection=True) + if hole_sub_poly is None: + return None, None + core_sub_polys.append(hole_sub_poly) + # check that None of the holes intersect one another + for i, c_pgon in enumerate(core_sub_polys): + for other_pgon in core_sub_polys[i + 1:]: + if Polygon2D._do_polygons_intersect(c_pgon, other_pgon): + return None, None + # if nothing intersects, we can build the perimeter polygons + out_polys = [polygon] + list(holes) + perimeter_sub_polys = [] + for p_count, (out_poly, in_poly) in enumerate(zip(out_polys, core_sub_polys)): + for out_seg, in_seg in zip(out_poly.segments, in_poly.segments): + if p_count == 0: + pts = (out_seg.p1, out_seg.p2, in_seg.p2, in_seg.p1) + else: + if not out_poly.is_clockwise: + pts = (out_seg.p1, in_seg.p1, in_seg.p2, out_seg.p2) + else: + (out_seg.p1, out_seg.p2, in_seg.p2, in_seg.p1) + perimeter_sub_polys.append(Polygon2D(pts)) + return perimeter_sub_polys, core_sub_polys
+ + + @staticmethod + def _do_polygons_intersect(polygon_1, polygon_2): + """Test to see if two polygons intersect one another.""" + for seg in polygon_1.segments: + for _s in polygon_2.segments: + if does_intersection_exist_line2d(seg, _s): + return True + return False + +
+[docs] + @staticmethod + def intersect_polygon_segments(polygon_list, tolerance): + """Intersect the line segments of a Polygon2D array to ensure matching segments. + + Specifically, this method checks a list of polygons in a pairwise manner to + see if one contains a vertex along an edge segment of the other within the + given tolerance. If so, the method creates a co-located vertex at that point, + partitioning the edge segment into two edge segments. Point ordering is + reserved within each Polygon2D and the order of Polygon2Ds within the input + polygon_list is also preserved. + + Args: + polygon_list: List of Polygon2Ds which will have their segments + intersected with one another. + tolerance: Distance within which two points are considered to be + co-located. + + Returns: + The input list of Polygon2D objects with extra vertices inserted + where necessary. + """ + for i in range(len(polygon_list) - 1): + # No need for j to start at 0 since two polygons are passed + # and they are compared against one other within intersect_segments. + for j in range(i + 1, len(polygon_list)): + polygon_list[i], polygon_list[j] = \ + Polygon2D.intersect_segments(polygon_list[i], polygon_list[j], + tolerance) + return polygon_list
+ + +
+[docs] + @staticmethod + def intersect_segments(polygon1, polygon2, tolerance): + """Intersect the line segments of two Polygon2Ds to ensure matching segments. + + Specifically, this method checks two adjacent polygons to see if one contains + a vertex along an edge segment of the other within the given tolerance. If so, + it creates a co-located vertex at that point, partitioning the edge segment + into two edge segments. Point ordering is preserved. + + Args: + polygon1: First polygon to check. + polygon2: Second polygon to check. + tolerance: Distance within which two points are considered to be co-located. + + Returns: + Two polygon objects with extra vertices inserted if necessary. + """ + polygon1_updates = [] + polygon2_updates = [] + + # bounding rectangle check + if not Polygon2D.overlapping_bounding_rect(polygon1, polygon2, tolerance): + return polygon1, polygon2 # no overlap + + # test if each point of polygon2 is within the tolerance distance of any segment + # of polygon1. If so, add the closest point on the segment to the polygon1 + # update list. And vice versa (testing polygon2 against polygon1). + for i1, seg1 in enumerate(polygon1.segments): + for i2, seg2 in enumerate(polygon2.segments): + # Test polygon1 against polygon2 + x = closest_point2d_on_line2d(seg2.p1, seg1) + if all(p.distance_to_point(x) > tolerance for p in polygon1.vertices) \ + and x.distance_to_point(seg2.p1) <= tolerance: + polygon1_updates.append((i1, x)) + # Test polygon2 against polygon1 + y = closest_point2d_on_line2d(seg1.p1, seg2) + if all(p.distance_to_point(y) > tolerance for p in polygon2.vertices) \ + and y.distance_to_point(seg1.p1) <= tolerance: + polygon2_updates.append((i2, y)) + + # apply any updates to polygon1 + polygon1 = Polygon2D._insert_updates_in_order(polygon1, polygon1_updates) + + # Apply any updates to polygon2 + polygon2 = Polygon2D._insert_updates_in_order(polygon2, polygon2_updates) + + return polygon1, polygon2
+ + +
+[docs] + @staticmethod + def overlapping_bounding_rect(polygon1, polygon2, tolerance): + """Check if the bounding rectangles of two polygons overlap within a tolerance. + + This is particularly useful as a check before performing computationally intense + processes between two polygons like intersection or boolean operations. + Checking the overlap of the bounding boxes is extremely quick with this + method's use of the the Separating Axis Theorem. + + Args: + polygon1: The first polygon to check. + polygon2: The second polygon to check. + tolerance: Distance within which two points are considered to be co-located. + """ + # Bounding rectangle check using the Separating Axis Theorem + polygon1_width = polygon1.max.x - polygon1.min.x + polygon2_width = polygon2.max.x - polygon2.min.x + dist_btwn_x = abs(polygon1.center.x - polygon2.center.x) + x_gap_btwn_rect = dist_btwn_x - (0.5 * polygon1_width) - (0.5 * polygon2_width) + + polygon1_height = polygon1.max.y - polygon1.min.y + polygon2_height = polygon2.max.y - polygon2.min.y + dist_btwn_y = abs(polygon1.center.y - polygon2.center.y) + y_gap_btwn_rect = dist_btwn_y - (0.5 * polygon1_height) - (0.5 * polygon2_height) + + if x_gap_btwn_rect > tolerance or y_gap_btwn_rect > tolerance: + return False # no overlap + return True # overlap exists
+ + +
+[docs] + @staticmethod + def group_by_overlap(polygons, tolerance): + """Group Polygon2Ds that overlap one another within the tolerance. + + This is useful as a pre-step before running Polygon2D.boolean_union_all() + in order to assess whether unionizing is necessary and to ensure that + it is only performed among the necessary groups of polygons. + + This method will return the minimal number of overlapping polygon groups + thanks to a recursive check of whether groups can be merged. + + Args: + polygons: A list of Polygon2D to be grouped by their overlapping. + tolerance: The minimum distance from the edge of a neighboring polygon + at which a point is considered to overlap with that polygon. + + Returns: + A list of lists where each sub-list represents a group of polygons + that all overlap with one another. + """ + # sort the polygons by area to help larger ones grab smaller ones + polygons = list(sorted(polygons, key=lambda x: x.area, reverse=True)) + + # loop through the polygons and check to see if it overlaps with the others + grouped_polys = [[polygons[0]]] + for poly in polygons[1:]: + group_found = False + for poly_group in grouped_polys: + for oth_poly in poly_group: + if poly.polygon_relationship(oth_poly, tolerance) >= 0: + poly_group.append(poly) + group_found = True + break + if group_found: + break + if not group_found: # the polygon does not overlap with any of the others + grouped_polys.append([poly]) # make a new group for the polygon + + # if some groups were found, recursively merge groups together + old_group_len = len(polygons) + while len(grouped_polys) != old_group_len: + new_groups, g_to_remove = grouped_polys[:], [] + for i, group_1 in enumerate(grouped_polys): + try: + for j, group_2 in enumerate(grouped_polys[i + 1:]): + if Polygon2D._groups_overlap(group_1, group_2, tolerance): + new_groups[i] = new_groups[i] + group_2 + g_to_remove.append(i + j + 1) + except IndexError: + pass # we have reached the end of the list of polygons + if len(g_to_remove) != 0: + g_to_remove = list(set(g_to_remove)) + g_to_remove.sort() + for ri in reversed(g_to_remove): + new_groups.pop(ri) + old_group_len = len(grouped_polys) + grouped_polys = new_groups + return grouped_polys
+ + + @staticmethod + def _groups_overlap(group_1, group_2, tolerance): + """Evaluate whether two groups of Polygons overlap with one another.""" + for poly_1 in group_1: + for poly_2 in group_2: + if poly_1.polygon_relationship(poly_2, tolerance) >= 0: + return True + return False + +
+[docs] + @staticmethod + def group_by_touching(polygons, tolerance): + """Group Polygon2Ds that touch or overlap one another within the tolerance. + + This is useful to group geometries together before extracting a bounding + rectangle or convex hull around multiple polygons. + + This method will return the minimal number of polygon groups + thanks to a recursive check of whether groups can be merged. + + Args: + polygons: A list of Polygon2D to be grouped by their touching. + tolerance: The minimum distance from the edge of a neighboring polygon + at which a point is considered to touch that polygon. + + Returns: + A list of lists where each sub-list represents a group of polygons + that all touch or overlap with one another. + """ + # sort the polygons by area to help larger ones grab smaller ones + polygons = list(sorted(polygons, key=lambda x: x.area, reverse=True)) + + # loop through the polygons and check to see if it overlaps with the others + grouped_polys = [[polygons[0]]] + for poly in polygons[1:]: + group_found = False + for poly_group in grouped_polys: + for oth_poly in poly_group: + if poly.does_polygon_touch(oth_poly, tolerance): + poly_group.append(poly) + group_found = True + break + if group_found: + break + if not group_found: # the polygon does not touch any of the others + grouped_polys.append([poly]) # make a new group for the polygon + + # if some groups were found, recursively merge groups together + old_group_len = len(polygons) + while len(grouped_polys) != old_group_len: + new_groups, g_to_remove = grouped_polys[:], [] + for i, group_1 in enumerate(grouped_polys): + try: + for j, group_2 in enumerate(grouped_polys[i + 1:]): + if Polygon2D._groups_touch(group_1, group_2, tolerance): + new_groups[i] = new_groups[i] + group_2 + g_to_remove.append(i + j + 1) + except IndexError: + pass # we have reached the end of the list of polygons + if len(g_to_remove) != 0: + g_to_remove = list(set(g_to_remove)) + g_to_remove.sort() + for ri in reversed(g_to_remove): + new_groups.pop(ri) + old_group_len = len(grouped_polys) + grouped_polys = new_groups + return grouped_polys
+ + + @staticmethod + def _groups_touch(group_1, group_2, tolerance): + """Evaluate whether two groups of Polygons touch with one another.""" + for poly_1 in group_1: + for poly_2 in group_2: + if poly_1.does_polygon_touch(poly_2, tolerance): + return True + return False + +
+[docs] + @staticmethod + def group_boundaries_and_holes(polygons, tolerance): + """Group polygons by whether they are contained within another. + + Args: + polygons: A list of Polygon2Ds to be grouped according to boundary and + holes within those boundaries. + tolerance: The minimum distance between points at which they are + considered distinct. + + Returns: + A list of lists where each sub-list contains Polygon2Ds and represents + one core geometry. Each sub-list will have has at least one Polygon2D + and, in the event that a core geometry has holes, there will be multiple + Polygon2Ds in the sub-list. The first item in the list will be the outer + boundary of the geometry and successive items represent hole polygons + contained within it. + """ + # first check to be sure that there isn't just one polygon + if len(polygons) == 1: + return [polygons] + # sort the polygons by area and separate base polygon from the remaining + polygons = sorted(polygons, key=lambda x: x.area, reverse=True) + base_poly = polygons[0] + remain_polys = list(polygons[1:]) + + # merge the smaller polygons into the larger polygons + merged_polys = [] + while len(remain_polys) > 0: + grp_poly = Polygon2D._match_holes_to_poly(base_poly, remain_polys, tolerance) + merged_polys.append(grp_poly) + if len(remain_polys) > 1: + base_poly = remain_polys[0] + del remain_polys[0] + elif len(remain_polys) == 1: # lone last Polygon2D + merged_polys.append([remain_polys[0]]) + del remain_polys[0] + return merged_polys
+ + + @staticmethod + def _match_holes_to_poly(base_poly, other_polys, tolerance): + """Attempt to merge other polygons into a base polygon as holes. + + Args: + base_poly: A Polygon2D to serve as the base. + other_polys: A list of other Polygon2D objects to attempt to merge into + the base_poly as a hole. This method will remove any Polygon2D + that are successfully merged into the output from this list. + tolerance: The minimum distance between points at which they are + considered distinct. + + Returns: + A list of Polygon2D where the first item is the base_poly and successive + items represent holes in this geometry. + """ + holes = [] + more_to_check = True + while more_to_check: + for i, r_poly in enumerate(other_polys): + if base_poly.polygon_relationship(r_poly, tolerance) == 1: + holes.append(r_poly) + del other_polys[i] + break + else: + more_to_check = False + return [base_poly] + holes + +
+[docs] + @staticmethod + def joined_intersected_boundary(polygons, tolerance): + """Get the boundary around several Polygon2D that are touching one another. + + This method is faster and more reliable than the gap_crossing_boundary + but requires that the Polygon2D be touching one another within the tolerance. + + Args: + polygons: The polygons to be joined into a boundary. These polygons + should have colinear vertices removed and they should not contain + degenerate polygons at the tolerance. The remove_colinear_vertices + method can be used to pre-process the input polygons to ensure they + meet these criteria. + tolerance: The tolerance at which the polygons are to be intersected + and then joined to give a resulting boundary. + + Returns: + A list of Polygon2D that represent the boundary around the input polygons. + Note that some of these Polygon2D may represent 'holes' within others + and it may be necessary to assess this when interpreting the result. + """ + # intersect the polygons with one another + int_poly = Polygon2D.intersect_polygon_segments(polygons, tolerance) + + # get indices of all unique vertices across the polygons + vertices = [] # collection of vertices as point objects + poly_indices = [] # collection of polygon indices + for loop in int_poly: + ind = [] + for v in loop: + found = False + for i, vert in enumerate(vertices): + if v.is_equivalent(vert, tolerance): + found = True + ind.append(i) + break + if not found: # add new point + vertices.append(v) + ind.append(len(vertices) - 1) + poly_indices.append(ind) + + # use the unique vertices to extract naked edges + edge_i = [] + edge_t = [] + for poly_i in poly_indices: + for i, vi in enumerate(poly_i): + try: # this can get slow for large number of vertices + ind = edge_i.index((vi, poly_i[i - 1])) + edge_t[ind] += 1 + except ValueError: # make sure reversed edge isn't there + try: + ind = edge_i.index((poly_i[i - 1], vi)) + edge_t[ind] += 1 + except ValueError: # add a new edge + if poly_i[i - 1] != vi: # avoid cases of same start and end + edge_i.append((poly_i[i - 1], vi)) + edge_t.append(0) + ext_edges = [] + for i, et in enumerate(edge_t): + if et == 0: + edg_ind = edge_i[i] + pts_2d = (vertices[edg_ind[0]], vertices[edg_ind[1]]) + ext_edges.append(LineSegment2D.from_end_points(*pts_2d)) + + # join the naked edges into closed polygons + outlines = Polyline2D.join_segments(ext_edges, tolerance) + closed_polys = [] + for bnd in outlines: + if isinstance(bnd, Polyline2D) and bnd.is_closed(tolerance): + closed_polys.append(bnd.to_polygon(tolerance)) + return closed_polys
+ + +
+[docs] + @staticmethod + def gap_crossing_boundary(polygons, min_separation, tolerance): + """Get the boundary around several Polygon2D, crossing gaps of min_separation. + + This method is less reliable than the joined_intersected_boundary because + higher values of min_separation that are greater than the lengths of polygon + segments can cause important details of the polygons to disappear. However, + when used appropriately, it can provide a boundary that jumps across gaps + to give resulting polygons that effectively bound all of the input polygons. + + Args: + polygons: The polygons to be joined into a boundary. These polygons + should have colinear vertices removed and they should not contain + degenerate polygons at the tolerance. The remove_colinear_vertices + method can be used to pre-process the input polygons to ensure they + meet these criteria. + min_separation: A number for the minimum distance between Polygon2D that + is considered a meaningful separation. In other words, this is + the maximum distance of the gap across + tolerance: The maximum difference between coordinate values of two + vertices at which they can be considered equivalent. + + Returns: + A list of Polygon2D that represent the boundary around the input polygons. + Note that some of these Polygon2D may represent 'holes' within others + and it may be necessary to assess this when interpreting the result. + """ + # ensure that all of the input polygons are counterclockwise + cclock_poly = [] + for poly in polygons: + if poly.is_clockwise: + cclock_poly.append(poly.reverse()) + else: + cclock_poly.append(poly) + + # determine which Polygon2D segments are 'exterior' using the min_separation + right_ang = -math.pi / 2 + ext_segs = [] + for i, poly in enumerate(cclock_poly): + # remove any short segments + rel_segs = [s for s in poly.segments if s.length > min_separation] + + # create min_separation line segments to be used to test intersection + test_segs = [] + for _s in rel_segs: + d_vec = _s.v.rotate(right_ang).normalize() + seg_pts = _s.subdivide(min_separation) + if len(seg_pts) <= 3: + seg_pts = [_s.midpoint] + else: + seg_pts = seg_pts[1:-1] + spec_test_segs = [] + for spt in seg_pts: + m_pt = spt.move(d_vec * -tolerance) + spec_test_segs.append(LineSegment2D(m_pt, d_vec * min_separation)) + test_segs.append(spec_test_segs) + + # intersect the test line segments to asses which parts are exterior + non_int_segs = [] + other_poly = [p for j, p in enumerate(cclock_poly) if j != i] + for j, (_s, int_lins) in enumerate(zip(rel_segs, test_segs)): + int_vals = [0] * len(int_lins) + for m, int_lin in enumerate(int_lins): + for _oth_p in other_poly: + if _oth_p.intersect_line_ray(int_lin): # intersection! + int_vals[m] = 1 + break + if sum(int_vals) == len(int_lins): # fully internal line + continue + else: # if the polygon is concave, also check for self intersection + if not poly.is_convex: + _other_segs = [x for k, x in enumerate(rel_segs) if k != j] + for m, int_lin in enumerate(int_lins): + for _oth_s in _other_segs: + if int_lin.intersect_line_ray(_oth_s) is not None: + int_vals[m] = 1 + break + + # determine the exterior segments using the intersections + check_sum = sum(int_vals) + if check_sum == 0: # fully external line + non_int_segs.append(_s) + elif len(int_vals) == 1 or check_sum == len(int_vals): + continue # fully internal line + else: # line that extends from inside to outside + # first see if the exterior part is meaningful + count_in_a_rows, repeat_count = [], 0 + for v in int_vals: + if v == 0: + repeat_count += 1 + count_in_a_rows.append(repeat_count) + else: + repeat_count = 0 + max_repeat = max(count_in_a_rows) + # if the exterior part is meaningful, split it + if max_repeat != 1: + last_pt = _s.p1 if int_vals[0] == 0 else None + for v, ts in zip(int_vals, int_lins): + if v == 0 and last_pt is None: + last_pt = ts.p1 + elif v == 1 and last_pt is not None: + lin_seg = LineSegment2D.from_end_points(last_pt, ts.p1) + last_pt = None + non_int_segs.append(lin_seg) + if last_pt is not None: + lin_seg = LineSegment2D.from_end_points(last_pt, _s.p2) + non_int_segs.append(lin_seg) + + ext_segs.extend(non_int_segs) + + # loop through exterior segments and add segments across the min_separation + joining_segs = [] + for i, e_seg in enumerate(ext_segs): + try: + for o_seg in ext_segs[i + 1:]: + dist, pts = closest_end_point2d_between_line2d(e_seg, o_seg) + if tolerance < dist <= min_separation: + joining_segs.append(LineSegment2D.from_end_points(*pts)) + except IndexError: + pass # we have reached the end of the list + + # join all of the segments together into polylines + all_segs = ext_segs + joining_segs + ext_bounds = Polyline2D.join_segments(all_segs, tolerance) + + # separate valid closed boundaries from open ones + closed_polys, open_bounds = [], [] + for bnd in ext_bounds: + if isinstance(bnd, Polyline2D) and bnd.is_closed(tolerance): + try: + closed_polys.append(bnd.to_polygon(tolerance)) + except AssertionError: # not a valid polygon + pass + else: + open_bounds.append(bnd) + + # if the resulting polylines are not closed, join the nearest end points + if len(closed_polys) != len(ext_bounds): + extra_segs = [] + for i, s_bnd in enumerate(open_bounds): + self_seg = LineSegment2D.from_end_points(s_bnd.p1, s_bnd.p2) + poss_segs = [self_seg] + try: + for o_bnd in open_bounds[i + 1:]: + pts = [ + (s_bnd.p1, o_bnd.p1), (s_bnd.p1, o_bnd.p2), + (s_bnd.p2, o_bnd.p1), (s_bnd.p2, o_bnd.p2)] + for comb in pts: + poss_segs.append(LineSegment2D.from_end_points(*comb)) + except IndexError: + continue # we have reached the end of the list + # sort the possible segments by their length + poss_segs.sort(key=lambda x: x.length, reverse=False) + if poss_segs[0] is self_seg: + extra_segs.append(poss_segs[0]) + else: # two possible connecting segments + extra_segs.append(poss_segs[0]) + extra_segs.append(poss_segs[1]) + # remove any duplicates from the extra segment list + non_dup_segs = [] + for e_seg in extra_segs: + for f_seg in non_dup_segs: + if e_seg.is_equivalent(f_seg, tolerance): + break + else: + non_dup_segs.append(e_seg) + extra_segs = non_dup_segs + # take the best available segments that fit the criteria + extra_segs.sort(key=lambda x: x.length, reverse=False) + extra_segs = extra_segs[:len(open_bounds)] + + # join all segments, hopefully into a final closed polyline + all_segs = ext_segs + joining_segs + extra_segs + ext_bounds = Polyline2D.join_segments(all_segs, tolerance) + closed_polys = [] + for bnd in ext_bounds: + if isinstance(bnd, Polyline2D) and bnd.is_closed(tolerance): + try: + closed_polys.append(bnd.to_polygon(tolerance)) + except AssertionError: # not a valid polygon + pass + + return closed_polys
+ + +
+[docs] + @staticmethod + def common_axes(polygons, direction, min_distance, merge_distance, angle_tolerance): + """Get LineSegment2Ds for the most common axes across a set of Polygon2Ds. + + This is often useful as a step before aligning a set of polygons to these + common axes. + + Args: + polygons: A list or tuple of Polygon2D objects for which common axes + will be evaluated. + direction: A Vector2D object to represent the direction in which the + common axes will be evaluated and generated + min_distance: The minimum distance at which common axes will be evaluated. + This value should typically be a little larger than the model + tolerance (eg. 5 to 20 times the tolerance) in order to ensure that + possible common axes across the input polygons are not missed. + merge_distance: The distance at which common axes next to one another + will be merged into a single axis. This should typically be 2-3 + times the min_distance in order to avoid generating several axes + that are immediately adjacent to one another. When using this + method to generate axes for alignment, this merge_distance should + be in the range of the alignment distance. + angle_tolerance: The max angle difference in radians that the polygon + segments direction can differ from the input direction before the + segments are not factored into this calculation of common axes. + + Returns: + A tuple with two elements. + + - common_axes: A list of LineSegment2D objects for the common + axes across the input polygons. + + - axis_values: A list of integers that aligns with the common_axes + and denotes how many segments of the input polygons each axis + relates to. Higher numbers indicate that that the axis is more + common among all of the possible axes. + """ + # gather the relevant segments of the input polygons + min_ang, max_ang = angle_tolerance, math.pi - angle_tolerance + rel_segs = [] + for p_gon in polygons: + for seg in p_gon.segments: + try: + s_ang = direction.angle(seg.v) + if s_ang < min_ang or s_ang > max_ang: + rel_segs.append(seg) + except ZeroDivisionError: # zero length segment to ignore + continue + if len(rel_segs) == 0: + return [], [] # none of the polygon segments are relevant in the direction + + # determine the extents around the polygons and the input direction + gen_vec = direction.rotate(math.pi / 2) + axis_angle = Vector2D(0, 1).angle_counterclockwise(gen_vec) + orient_poly = polygons + if axis_angle != 0: # rotate geometry to the bounding box + cpt = polygons[0].vertices[0] + orient_poly = [pl.rotate(-axis_angle, cpt) for pl in polygons] + xx = Polygon2D._bounding_domain_x(orient_poly) + yy = Polygon2D._bounding_domain_y(orient_poly) + min_pt = Point2D(xx[0], yy[0]) + max_pt = Point2D(xx[1], yy[1]) + if axis_angle != 0: # rotate the points back + min_pt = min_pt.rotate(axis_angle, cpt) + max_pt = max_pt.rotate(axis_angle, cpt) + + # generate all possible axes from the extents and min_distance + axis_vec = direction.normalize() * (xx[1] - xx[0]) + incr_vec = gen_vec.normalize() * (min_distance) + current_pt = min_pt + current_dist, max_dist = 0, yy[1] - yy[0] + all_axes = [] + while current_dist < max_dist: + axis = LineSegment2D(current_pt, axis_vec) + all_axes.append(axis) + current_pt = current_pt.move(incr_vec) + current_dist += min_distance + + # evaluate the axes based on how many relevant segments they are next to + mid_pts = [seg.midpoint for seg in rel_segs] + rel_axes, axes_value = [], [] + for axis in all_axes: + axis_val = 0 + for pt in mid_pts: + close_pt = closest_point2d_on_line2d_infinite(pt, axis) + if close_pt.distance_to_point(pt) <= min_distance: + axis_val += 1 + if axis_val != 0: + rel_axes.append(axis) + axes_value.append(axis_val) + if len(rel_axes) == 0: + return [], [] # none of the generated axes are relevant + + # group the axes by proximity + last_ax = rel_axes[0] + axes_groups = [[last_ax]] + group_values = [[axes_value[0]]] + for axis, val in zip(rel_axes[1:], axes_value[1:]): + if axis.p.distance_to_point(last_ax.p) <= merge_distance: + axes_groups[-1].append(axis) + group_values[-1].append(val) + else: # start a new group + axes_groups.append([axis]) + group_values.append([val]) + last_ax = axis + + # average the line segments that are within the merge_distance of one another + axis_values = [max(val) for val in group_values] + common_axes = [] + for ax_group, grp_vals in zip(axes_groups, group_values): + if len(ax_group) == 1: + common_axes.append(ax_group[0]) + else: + index_max = max(range(len(grp_vals)), key=grp_vals.__getitem__) + common_axes.append(ax_group[index_max]) + return common_axes, axis_values
+ + + @staticmethod + def _bounding_domain_x(geometries): + """Get minimum and maximum X coordinates of multiple polygons.""" + min_x, max_x = geometries[0].min.x, geometries[0].max.x + for geom in geometries[1:]: + if geom.min.x < min_x: + min_x = geom.min.x + if geom.max.x > max_x: + max_x = geom.max.x + return min_x, max_x + + @staticmethod + def _bounding_domain_y(geometries): + """Get minimum and maximum Y coordinates of multiple polygons.""" + min_y, max_y = geometries[0].min.y, geometries[0].max.y + for geom in geometries[1:]: + if geom.min.y < min_y: + min_y = geom.min.y + if geom.max.y > max_y: + max_y = geom.max.y + return min_y, max_y + + def _point_in_polygon(self, tolerance): + """Get a Point2D that is always reliably inside this Polygon2D. + + The point will be close to the edge of the Polygon but it will always + be inside it for all concave geometries. Furthermore, it is relatively + fast compared with computing the pole_of_inaccessibility. + """ + try: + poly = self.remove_colinear_vertices(tolerance) + move_vec, v_angle = self._inward_pointing_vec(poly) + except (AssertionError, ZeroDivisionError): # zero area Polygon2D; use center + return self.center + + move_vec = move_vec * ((tolerance / math.sin(v_angle / 2)) + 0.00001) + point_in_poly = poly.vertices[0] + move_vec + if not self.is_point_inside(point_in_poly): + point_in_poly = poly.vertices[0] - move_vec + return point_in_poly + + @staticmethod + def _inward_pointing_vec(polygon): + """Get a unit vector pointing inward/outward from the first vertex of the Polygon + """ + v1 = polygon.vertices[-1] - polygon.vertices[0] + v2 = polygon.vertices[1] - polygon.vertices[0] + v_angle = v1.angle(v2) + if v_angle == math.pi: # colinear vertices; prevent averaging to zero + rgt_ang = math.pi / 2 + return v1.rotate(rgt_ang).normalize(), rgt_ang + else: # average the two edge vectors together + avg_coords = ((v1.x + v2.x) / 2), ((v1.y + v2.y) / 2) + return Vector2D(*avg_coords).normalize(), v_angle + + def _transfer_properties(self, new_polygon): + """Transfer properties from this polygon to a new polygon. + + This is used by the transform methods that don't alter the relationship of + face vertices to one another (move, rotate, reflect). + """ + new_polygon._perimeter = self._perimeter + new_polygon._area = self._area + new_polygon._is_convex = self._is_convex + new_polygon._is_self_intersecting = self._is_self_intersecting + new_polygon._is_clockwise = self._is_clockwise + + @staticmethod + def _get_centroid_cell(polygon): + """Get a Cell object at the centroid of the Polygon2D.""" + area = 0 + x = 0 + y = 0 + b = polygon[-1] # prev + for a in polygon: + f = a[0] * b[1] - b[0] * a[1] + x += (a[0] + b[0]) * f + y += (a[1] + b[1]) * f + area += f * 3 + b = a + if area == 0: + return _Cell(polygon[0][0], polygon[0][1], 0, polygon) + return _Cell(x / area, y / area, 0, polygon) + + @staticmethod + def _insert_updates_in_order(polygon, polygon_updates): + """Insert updates from the intersect_segments method into a polygon. + + This method ensures that multiple updates to a single segment are inserted + in the correct order over the polygon. + + Args: + polygon: A Polygon2D to be updated with intersection points + polygon_updates: A list of tuples where each tuple has two values. The + first is the index of the segment to be updated and the second is + the point to insert. + """ + polygon_updates.sort(key=lambda x: x[0]) # sort updates by order of insertion + poly_points = list(polygon.vertices) # convert tuple to mutable list + last_i = -1 + colinear_count = 0 + for update in polygon_updates[::-1]: # traverse backwards to preserve order + new_i = update[0] + 1 + if new_i == last_i: # check order of new intersections on the same segment + colinear_count += 1 + p1 = poly_points[update[0]] + for i, pt in enumerate(poly_points[new_i:new_i + colinear_count]): + if p1.distance_to_point(pt) > p1.distance_to_point(update[1]): + poly_points.insert(new_i + i, update[1]) + break + else: + poly_points.insert(new_i + colinear_count, update[1]) + else: + colinear_count = 0 + poly_points.insert(new_i, update[1]) + last_i = new_i + return Polygon2D(poly_points) + + @staticmethod + def _segments_from_vertices(vertices): + _segs = [] + for i, vert in enumerate(vertices): + _seg = LineSegment2D.from_end_points(vertices[i - 1], vert) + _segs.append(_seg) + _segs.append(_segs.pop(0)) # segments will start from the first point + return _segs + + @staticmethod + def _merge_boundary_and_holes(boundary, holes, split=False): + """Return a list of points for a boundary merged with all holes. + + The holes are merged one-by-one using the shortest distance from all of + the holes to the boundary to ensure one hole's seam does not cross another. + The run time of this method scales linearly with the total number of + vertices, which makes it significantly better for shapes with many holes + compared to recursively calling the Polygon2D._merge_boundary_and_closest_hole + method. However, it is not nearly as efficient as the method used in + Polygon2D.from_shape_with_holes_fast, which is typically not as beautiful + of a result as this method. + + Args: + boundary: A list of Point2D objects for the outer boundary inside of + which the hole is contained. + hole: A list of lists where each sub-list represents a hole and contains + several Point2D objects that represent the hole. + split: A boolean to note whether the last hole should be merged into + the boundary, effectively splitting the shape into two lists of + vertices instead of a single list. This is useful when trying to + translate a shape with holes to a platform that does not support + holes or struggles with single lists of vertices that wind inward + to cut out the holes since this option returns two "normal" concave + polygons. However, it is possible that the shape cannot be + reliably split this way and, in this case, this method will + return None. (Default: False). + + Returns: + A single list of vertices with the holes merged into the boundary. When + split is True and the splitting is successful, this will be two lists + of vertices for the split shapes. If splitting was not successful, + this method will return None and, if a hole-less split shape is + still required, it is recommended that triangulation be used to get + the hole-less shapes. + """ + # compute the initial distances between the holes and the boundary + original_boundary = boundary[:] + hole_dicts, min_dists = [], [] + for hole in holes: + dist_dict = {} + for i, b_pt in enumerate(boundary): + for j, h_pt in enumerate(hole): + dist_dict[b_pt.distance_to_point(h_pt)] = [i, j] + hole_dicts.append(dist_dict) + min_dists.append(min(dist_dict.keys())) + # merge each hole into the boundary, moving progressively by minimum distance + final_hole_count = 0 if not split else 1 + while len(holes) > final_hole_count: + # merge the hole into the boundary + hole_index = min_dists.index(min(min_dists)) + boundary, old_hole, b_ind = Polygon2D._merge_boundary_and_hole_detailed( + boundary, holes[hole_index], hole_dicts[hole_index]) + # remove the hole from the older lists + hole_dicts.pop(hole_index) + holes.pop(hole_index) + # update the hole_dicts based on the new boundary + add_ind = len(old_hole) + for hd in hole_dicts: + for ind_list in hd.values(): + if ind_list[0] > b_ind: + ind_list[0] += add_ind + # add the distances from the old hole to the remaining holes to hole_dicts + old_hole_ind = [b_ind + i for i in range(add_ind)] + for hole, dist_dict in zip(holes, hole_dicts): + for bi, b_pt in zip(old_hole_ind, old_hole): + for j, h_pt in enumerate(hole): + dist_dict[b_pt.distance_to_point(h_pt)] = [bi, j] + # generated an updated min_dists list + min_dists = [min(dist_dict.keys()) for dist_dict in hole_dicts] + if not split: + return boundary + + # sort the distances to find the closest point + last_hd = hole_dicts[0] + sort_dist = sorted(last_hd.keys()) + # find the closest connection between the hole and the original boundary polygon + p1_indices = dist_dict[sort_dist[0]] + p2_index = 1 + p2_indices = dist_dict[sort_dist[p2_index]] + p2_bound_pt = boundary[p2_indices[0]] + while p2_bound_pt not in original_boundary: + p2_index += 1 + p2_indices = dist_dict[sort_dist[p2_index]] + p2_bound_pt = boundary[p2_indices[0]] + p2_hole_pt = hole[p2_indices[1]] + # merge the hole into the boundary + hole_deque = deque(hole) + hole_deque.rotate(-p1_indices[1]) + hole_insert = [boundary[p1_indices[0]]] + list(hole_deque) + \ + [hole[p1_indices[1]]] + boundary[p1_indices[0]:p1_indices[0]] = hole_insert + # use the second most distant points to split the shape + p2_bound_i = boundary.index(p2_bound_pt) + p2_hole_i = boundary.index(p2_hole_pt) + if p2_hole_i < p2_bound_i: + boundary_1 = boundary[p2_hole_i:p2_bound_i + 1] + boundary_2 = boundary[:p2_hole_i + 1] + boundary[p2_bound_i:] + else: + boundary_1 = boundary[p2_bound_i:p2_hole_i + 1] + boundary_2 = boundary[:p2_bound_i + 1] + boundary[p2_hole_i:] + poly_1, poly_2 = Polygon2D(boundary_1), Polygon2D(boundary_2) + + # if the split polygons are self-intersecting, try to find a solution + p2_index = 0 + while poly_1.is_self_intersecting or poly_2.is_self_intersecting: + p2_index += 1 + try: + p2_indices = dist_dict[sort_dist[p2_index]] + except IndexError: # no solution was found; just return None + return None + p2_bound_pt = boundary[p2_indices[0]] + p2_hole_pt = hole[p2_indices[1]] + p2_bound_i = boundary.index(p2_bound_pt) + p2_hole_i = boundary.index(p2_hole_pt) + if p2_hole_i < p2_bound_i: + boundary_1 = boundary[p2_hole_i:p2_bound_i + 1] + boundary_2 = boundary[:p2_hole_i + 1] + boundary[p2_bound_i:] + else: + boundary_1 = boundary[p2_bound_i:p2_hole_i + 1] + boundary_2 = boundary[:p2_bound_i + 1] + boundary[p2_hole_i:] + try: + poly_1, poly_2 = Polygon2D(boundary_1), Polygon2D(boundary_2) + except AssertionError: + pass # the polygons are not valid; keep searching + return boundary_1, boundary_2 + + @staticmethod + def _merge_boundary_and_hole_detailed(boundary, hole, dist_dict): + """Create a single list of points with a hole and boundary. + + This method will also return the newly-added vertices of the hole as + well as the index of where the hole was inserted in the larger boundary. + + Args: + boundary: A list of Point2D objects for the outer boundary inside of + which the hole is contained. + hole: A list of Point2D objects for the hole. + dist_dict: A dictionary with keys of distances between each of the points + in the boundary and hole lists and values as tuples with two values: + (the index of the boundary point, the index of the hole point) + + Returns: + A tuple with three values + + - boundary: A single list of vertices with the input hole merged + into the boundary. + + - hole_insert: A list of vertices representing the hole that has + been inserted into the boundary. + + - insert_index: An integer for where the hole was inserted in the + boundary. + """ + min_dist = min(dist_dict.keys()) + min_indexes = dist_dict[min_dist] + hole_deque = deque(hole) + hole_deque.rotate(-min_indexes[1]) + hole_insert = [boundary[min_indexes[0]]] + list(hole_deque) + \ + [hole[min_indexes[1]]] + boundary[min_indexes[0]:min_indexes[0]] = hole_insert + insert_index = min_indexes[0] + return boundary, hole_insert, insert_index + + @staticmethod + def _merge_boundary_and_closest_hole(boundary, holes): + """Return a list of points for a boundary merged with the closest hole.""" + hole_dicts = [] + min_dists = [] + for hole in holes: + dist_dict = {} + for i, b_pt in enumerate(boundary): + for j, h_pt in enumerate(hole): + dist_dict[b_pt.distance_to_point(h_pt)] = (i, j) + hole_dicts.append(dist_dict) + min_dists.append(min(dist_dict.keys())) + hole_index = min_dists.index(min(min_dists)) + new_boundary = Polygon2D._merge_boundary_and_hole( + boundary, holes[hole_index], hole_dicts[hole_index]) + holes.pop(hole_index) + return new_boundary, holes + + @staticmethod + def _merge_boundary_and_hole(boundary, hole, dist_dict): + """Create a single list of points describing a boundary shape with a hole. + + Args: + boundary: A list of Point2D objects for the outer boundary inside of + which the hole is contained. + hole: A list of Point2D objects for the hole. + dist_dict: A dictionary with keys of distances between each of the points + in the boundary and hole lists and values as tuples with two values: + (the index of the boundary point, the index of the hole point) + """ + min_dist = min(dist_dict.keys()) + min_indexes = dist_dict[min_dist] + hole_deque = deque(hole) + hole_deque.rotate(-min_indexes[1]) + hole_insert = [boundary[min_indexes[0]]] + list(hole_deque) + \ + [hole[min_indexes[1]]] + boundary[min_indexes[0]:min_indexes[0]] = hole_insert + return boundary + + @staticmethod + def _are_clockwise(vertices): + """Check if a list of vertices are clockwise. + + This is a quicker calculation when all you need is the direction and not area. + """ + _a = 0 + for i, pt in enumerate(vertices): + _a += vertices[i - 1].x * pt.y - vertices[i - 1].y * pt.x + return _a < 0 + + def __copy__(self): + _new_poly = Polygon2D(self._vertices) + _new_poly._segments = self._segments + _new_poly._perimeter = self._perimeter + _new_poly._area = self._area + _new_poly._is_clockwise = self._is_clockwise + _new_poly._is_convex = self._is_convex + _new_poly._is_self_intersecting = self._is_self_intersecting + return _new_poly + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return tuple(hash(pt) for pt in self._vertices) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Polygon2D) and self.__key() == other.__key() + + def __repr__(self): + return 'Polygon2D ({} vertices)'.format(len(self))
+ + + +class _Cell(object): + """2D cell object used in certain Polygon computations (eg. pole_of_inaccessibility). + + Args: + x: The X coordinate of the cell origin. + y: The Y coordinate of the cell origin. + h: The dimension of the cell. + polygon: An array representation of a Polygon2D. + + Properties: + * x + * y + * h + * d + * max + """ + __slots__ = ('x', 'y', 'h', 'd', 'max') + + def __init__(self, x, y, h, polygon): + self.h = h + self.y = y + self.x = x + self.d = self._point_to_polygon_distance(x, y, polygon) + self.max = self.d + self.h * math.sqrt(2) + + def _point_to_polygon_distance(self, x, y, polygon): + """Get the distance from an X,Y point to the edge of a Polygon.""" + inside = False + min_dist_sq = inf + + b = polygon[-1] + for a in polygon: + + if (a[1] > y) != (b[1] > y) and \ + (x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0]): + inside = not inside + + min_dist_sq = min(min_dist_sq, self._get_seg_dist_sq(x, y, a, b)) + b = a + + result = math.sqrt(min_dist_sq) + if not inside: + return -result + return result + + @staticmethod + def _get_seg_dist_sq(px, py, a, b): + """Get the squared distance from a point to a segment.""" + x = a[0] + y = a[1] + dx = b[0] - x + dy = b[1] - y + + if dx != 0 or dy != 0: + t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy) + + if t > 1: + x = b[0] + y = b[1] + + elif t > 0: + x += dx * t + y += dy * t + + dx = px - x + dy = py - y + + return dx * dx + dy * dy + + def __lt__(self, other): + return self.max < other.max + + def __lte__(self, other): + return self.max <= other.max + + def __gt__(self, other): + return self.max > other.max + + def __gte__(self, other): + return self.max >= other.max + + def __eq__(self, other): + return self.max == other.max +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry2d/polyline.html b/docs/_modules/ladybug_geometry/geometry2d/polyline.html new file mode 100644 index 00000000..59c12653 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry2d/polyline.html @@ -0,0 +1,1604 @@ + + + + + + + ladybug_geometry.geometry2d.polyline — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry2d.polyline

+# coding=utf-8
+"""2D Polyline"""
+from __future__ import division
+import math
+
+from ._2d import Base2DIn2D
+from .pointvector import Point2D
+from .line import LineSegment2D
+from ..intersection2d import intersect_line2d, intersect_line2d_infinite
+from .._polyline import _group_vertices
+
+
+
+[docs] +class Polyline2D(Base2DIn2D): + """2D polyline object. + + Args: + vertices: A list of Point2D objects representing the vertices of the polyline. + interpolated: Boolean to note whether the polyline should be interpolated + between the input vertices when it is translated to other interfaces. + Note that this property has no bearing on the geometric calculations + performed by this library and is only present in order to assist with + display/translation. + + Properties: + * vertices + * segments + * min + * max + * center + * p1 + * p2 + * length + * is_self_intersecting + * interpolated + """ + __slots__ = ('_interpolated', '_segments', '_length', '_is_self_intersecting') + + def __init__(self, vertices, interpolated=False): + """Initialize Polyline2D.""" + Base2DIn2D.__init__(self, vertices) + self._interpolated = interpolated + self._segments = None + self._length = None + self._is_self_intersecting = None + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Polyline2D from a dictionary. + + Args: + data: A python dictionary in the following format. + + .. code-block:: python + + { + "type": "Polyline2D", + "vertices": [(0, 0), (10, 0), (0, 10)] + } + """ + interp = data['interpolated'] if 'interpolated' in data else False + return cls(tuple(Point2D.from_array(pt) for pt in data['vertices']), interp)
+ + +
+[docs] + @classmethod + def from_array(cls, point_array): + """Create a Polyline2D from a nested array of vertex coordinates. + + Args: + point_array: Nested array of point arrays. + """ + return Polyline2D(Point2D(*point) for point in point_array)
+ + +
+[docs] + @classmethod + def from_polygon(cls, polygon): + """Create a closed Polyline2D from a Polygon2D. + + Args: + polygon: A Polygon2D object to be converted to a Polyline2D. + """ + return Polyline2D(polygon.vertices + (polygon.vertices[0],))
+ + + @property + def segments(self): + """Tuple of all line segments in the polyline.""" + if self._segments is None: + self._segments = \ + tuple(LineSegment2D.from_end_points(vert, self._vertices[i + 1]) + for i, vert in enumerate(self._vertices[:-1])) + return self._segments + + @property + def length(self): + """The length of the polyline.""" + if self._length is None: + self._length = sum([seg.length for seg in self.segments]) + return self._length + + @property + def p1(self): + """Starting point of the Polyline2D.""" + return self._vertices[0] + + @property + def p2(self): + """End point of the Polyline2D.""" + return self._vertices[-1] + + @property + def is_self_intersecting(self): + """Boolean noting whether the polyline has self-intersecting segments.""" + if self._is_self_intersecting is None: + self._is_self_intersecting = False + _segs = self.segments + for i, _s in enumerate(_segs[1: len(_segs) - 1]): + _skip = (i, i + 1, i + 2) + _other_segs = [x for j, x in enumerate(_segs) if j not in _skip] + for _oth_s in _other_segs: + if _s.intersect_line_ray(_oth_s) is not None: # intersection! + self._is_self_intersecting = True + break + if self._is_self_intersecting is True: + break + return self._is_self_intersecting + + @property + def interpolated(self): + """Boolean noting whether the polyline should be interpolated upon translation. + + Note that this property has no bearing on the geometric calculations + performed by this library and is only present in order to assist with + display/translation. + """ + return self._interpolated + +
+[docs] + def is_closed(self, tolerance): + """Test whether this polyline is closed to within the tolerance. + + Args: + tolerance: The minimum difference between vertices below which vertices + are considered the same. + """ + return self._vertices[0].is_equivalent(self._vertices[-1], tolerance)
+ + +
+[docs] + def remove_colinear_vertices(self, tolerance): + """Get a version of this polyline without colinear or duplicate vertices. + + Args: + tolerance: The minimum distance that a vertex can be from a line + before it is considered colinear. + """ + if len(self.vertices) == 3: + return self # Polyline2D cannot have fewer than 3 vertices + new_vertices = [self.vertices[0]] # first vertex is always ok + skip = 0 # track the number of vertices being skipped/removed + # loop through vertices and remove all cases of colinear verts + for i, _v in enumerate(self.vertices[1:-1]): + _a = self[i - skip].determinant(_v) + _v.determinant(self[i + 2]) + \ + self[i + 2].determinant(self[i - skip]) + if abs(_a) >= tolerance: + new_vertices.append(_v) + skip = 0 + else: + skip += 1 + new_vertices.append(self[-1]) # last vertex is always ok + _new_poly = Polyline2D(new_vertices) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def reverse(self): + """Get a copy of this polyline where the vertices are reversed.""" + _new_poly = Polyline2D(tuple(pt for pt in reversed(self.vertices))) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def move(self, moving_vec): + """Get a polyline that has been moved along a vector. + + Args: + moving_vec: A Vector2D with the direction and distance to move the polyline. + """ + _new_poly = Polyline2D(tuple(pt.move(moving_vec) for pt in self.vertices)) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def rotate(self, angle, origin): + """Get a polyline that is rotated counterclockwise by a certain angle. + + Args: + angle: An angle for rotation in radians. + origin: A Point2D for the origin around which the point will be rotated. + """ + _new_poly = Polyline2D(tuple(pt.rotate(angle, origin) for pt in self.vertices)) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a polyline reflected across a plane with the input normal and origin. + + Args: + normal: A Vector2D representing the normal vector for the plane across + which the polyline will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point2D representing the origin from which to reflect. + """ + _new_poly = Polyline2D(tuple(pt.reflect(normal, origin) for pt in self.vertices)) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a polyline by a factor from an origin point. + + Args: + factor: A number representing how much the polyline should be scaled. + origin: A Point2D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0). + """ + if origin is None: + _new_poly = Polyline2D(tuple( + Point2D(pt.x * factor, pt.y * factor) for pt in self.vertices)) + else: + _new_poly = Polyline2D(tuple( + pt.scale(factor, origin) for pt in self.vertices)) + _new_poly._interpolated = self._interpolated + return _new_poly
+ + +
+[docs] + def offset(self, distance, check_intersection=False): + """Offset the polyline by a given distance. + + Note that the resulting shape may be self-intersecting if the distance + is large enough and the is_self_intersecting property may be used to identify + these shapes. + + Args: + distance: The distance that the polyline will be offset. Both positive + and negative values are accepted with positive values being offset + to the left of the polyline line and negative values being offset + to the right of the polyline (starting from the first polyline point + and continuing down the polyline). + check_intersection: A boolean to note whether the resulting operation + should be checked for self intersection and, if so, None will be + returned instead of the self-intersecting polyline. + """ + # make sure the offset is not zero + if distance == 0: + return self + + # loop through the vertices and get the new offset vectors + middle_verts = list(self._vertices[1:-1]) + if len(middle_verts) != 1: + middle_verts = [pt for i, pt in enumerate(middle_verts) + if pt != middle_verts[i - 1]] + all_verts = [self._vertices[0]] + middle_verts + [self._vertices[-1]] + move_vec_st = self.segments[0].v.rotate(math.pi / 2).normalize() * distance + move_vecs = [move_vec_st] + for i, pt in enumerate(middle_verts): + v1 = all_verts[i] - pt + v2 = all_verts[i + 2] - pt + ang = v1.angle_counterclockwise(v2) / 2 + if ang == 0: + ang = math.pi / 2 + m_vec = v1.rotate(ang).normalize() + m_dist = -distance / math.sin(ang) + m_vec = m_vec * m_dist + move_vecs.append(m_vec) + move_vec_end = self.segments[-1].v.rotate(math.pi / 2).normalize() * distance + move_vecs.append(move_vec_end) + + # move the vertices by the offset to create the new Polygon2D + new_pts = tuple(pt.move(m_vec) for pt, m_vec in zip(all_verts, move_vecs)) + new_poly = Polyline2D(new_pts, self.interpolated) + + # check for self intersection between the moving vectors if requested + if check_intersection: + poly_segs = new_poly.segments + _segs = [LineSegment2D(p, v) for p, v in zip(all_verts, move_vecs)] + _skip = (0, len(_segs) - 1) + _other_segs = [x for j, x in enumerate(poly_segs) if j not in _skip] + for _oth_s in _other_segs: + if _segs[0].intersect_line_ray(_oth_s) is not None: # intersection! + return None + for i, _s in enumerate(_segs[1: len(_segs)]): + _skip = (i, i + 1) + _other_segs = [x for j, x in enumerate(poly_segs) if j not in _skip] + for _oth_s in _other_segs: + if _s.intersect_line_ray(_oth_s) is not None: # intersection! + return None + return new_poly
+ + +
+[docs] + def intersect_line_ray(self, line_ray): + """Get the intersections between this polyline and a Ray2D or LineSegment2D. + + Args: + line_ray: A LineSegment2D or Ray2D or to intersect. + + Returns: + A list with Point2D objects for the intersections. + List will be empty if no intersection exists. + """ + intersections = [] + for _s in self.segments: + inters = intersect_line2d(_s, line_ray) + if inters is not None: + intersections.append(inters) + return intersections
+ + +
+[docs] + def intersect_line_infinite(self, ray): + """Get the intersections between this polyline and a Ray2D extended infinitely. + + Args: + ray: A Ray2D or to intersect. This will be extended in both + directions infinitely for the intersection. + + Returns: + A list with Point2D objects for the intersections. + List will be empty if no intersection exists. + """ + intersections = [] + for _s in self.segments: + inters = intersect_line2d_infinite(_s, ray) + if inters is not None: + intersections.append(inters) + return intersections
+ + +
+[docs] + def to_dict(self): + """Get Polyline2D as a dictionary.""" + base = {'type': 'Polyline2D', + 'vertices': [pt.to_array() for pt in self.vertices]} + if self.interpolated: + base['interpolated'] = self.interpolated + return base
+ + +
+[docs] + def to_array(self): + """Get a list of lists where each sub-list represents a Point2D vertex.""" + return tuple(pt.to_array() for pt in self.vertices)
+ + +
+[docs] + def to_polygon(self, tolerance): + """Get a Polygon2D derived from this object. + + If the polyline is closed to within the tolerance, the segments of this + polyline and the resulting polygon will match. Otherwise, an extra + LineSegment2D will be added to connect the start and end of the polyline. + + Args: + tolerance: The minimum difference between vertices below which vertices + are considered the same. + """ + from .polygon import Polygon2D # must be imported here to avoid circular import + if self.is_closed(tolerance): + return Polygon2D(self._vertices[:-1]) + return Polygon2D(self._vertices)
+ + +
+[docs] + @staticmethod + def join_segments(segments, tolerance): + """Get an array of Polyline2Ds from a list of LineSegment2Ds. + + Args: + segments: An array of LineSegment2D objects. + tolerance: The minimum difference in X, Y, and Z values at which Point2Ds + are considered equivalent. Segments with points that match within the + tolerance will be joined. + + Returns: + An array of Polyline2D and LineSegment2D objects assembled from the + joined segments. + """ + # group the vertices that make up polylines + grouped_verts = _group_vertices(segments, tolerance) + + # create the Polyline2D and LineSegment2D objects + joined_lines = [] + for v_list in grouped_verts: + if len(v_list) == 2: + joined_lines.append(LineSegment2D.from_end_points(v_list[0], v_list[1])) + else: + joined_lines.append(Polyline2D(v_list)) + return joined_lines
+ + + def _transfer_properties(self, new_polyline): + """Transfer properties from this polyline to a new polyline.""" + new_polyline._interpolated = self._interpolated + new_polyline._length = self._length + new_polyline._is_self_intersecting = self._is_self_intersecting + + def __copy__(self): + return Polyline2D(self._vertices, self._interpolated) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return tuple(hash(pt) for pt in self._vertices) + (self._interpolated,) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Polyline2D) and self.__key() == other.__key() + + def __repr__(self): + return 'Polyline2D ({} vertices)'.format(len(self))
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry2d/ray.html b/docs/_modules/ladybug_geometry/geometry2d/ray.html new file mode 100644 index 00000000..eb9c28b8 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry2d/ray.html @@ -0,0 +1,1285 @@ + + + + + + + ladybug_geometry.geometry2d.ray — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry2d.ray

+# coding=utf-8
+"""2D Ray"""
+from __future__ import division
+
+from .pointvector import Vector2D, Point2D
+from ._1d import Base1DIn2D
+
+
+
+[docs] +class Ray2D(Base1DIn2D): + """2D Ray object. + + Args: + p: A Point2D representing the base of the ray. + v: A Vector2D representing the direction of the ray. + + Properties: + * p + * v + * min + * max + * center + """ + __slots__ = () + + def __init__(self, p, v): + """Initialize Ray2D.""" + Base1DIn2D.__init__(self, p, v) + +
+[docs] + @classmethod + def from_array(cls, ray_array): + """ Create a Ray2D from a nested array with a point and a vector. + + Args: + ray_array: Nested tuples ((p.x, p.y), (v.x, v.y)). + """ + return Ray2D(Point2D(*ray_array[0]), Vector2D(*ray_array[1]))
+ + +
+[docs] + def reverse(self): + """Get a copy of this ray that is reversed.""" + return Ray2D(self.p, self.v.reverse())
+ + +
+[docs] + def move(self, moving_vec): + """Get a ray that has been moved along a vector. + + Args: + moving_vec: A Vector2D with the direction and distance to move the ray. + """ + return Ray2D(self.p.move(moving_vec), self.v)
+ + +
+[docs] + def rotate(self, angle, origin): + """Get a ray that is rotated counterclockwise by a certain angle. + + Args: + angle: An angle for rotation in radians. + origin: A Point2D for the origin around which the ray will be rotated. + """ + return Ray2D(self.p.rotate(angle, origin), self.v.rotate(angle))
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a ray reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector2D representing the normal vector for the plane across + which the ray will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point2D representing the origin from which to reflect. + """ + return Ray2D(self.p.reflect(normal, origin), self.v.reflect(normal))
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a ray by a factor from an origin point. + + Args: + factor: A number representing how much the ray should be scaled. + origin: A Point2D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0). + """ + return Ray2D(self.p.scale(factor, origin), self.v * factor)
+ + +
+[docs] + def to_dict(self): + """Get Ray2D as a dictionary.""" + base = Base1DIn2D.to_dict(self) + base['type'] = 'Ray2D' + return base
+ + +
+[docs] + def to_array(self): + """A nested array representing the start point and vector.""" + return (self.p.to_array(), self.v.to_array())
+ + + def _u_in(self, u): + return u >= 0.0 + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (hash(self.p), hash(self.v)) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Ray2D) and self.__key() == other.__key() + + def __repr__(self): + return 'Ray2D (point <%.2f, %.2f>) (vector <%.2f, %.2f>)' % \ + (self.p.x, self.p.y, self.v.x, self.v.y)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/arc.html b/docs/_modules/ladybug_geometry/geometry3d/arc.html new file mode 100644 index 00000000..56aac96d --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/arc.html @@ -0,0 +1,1655 @@ + + + + + + + ladybug_geometry.geometry3d.arc — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.arc

+# coding=utf-8
+"""3D Arc"""
+from __future__ import division
+
+from .plane import Plane
+
+from ..geometry2d.pointvector import Point2D, Vector2D
+from ..geometry2d.ray import Ray2D
+from ..geometry2d.arc import Arc2D
+from .pointvector import Vector3D, Point3D
+from .polyline import Polyline3D
+
+import math
+
+
+
+[docs] +class Arc3D(object): + """3D arc object. + + Args: + plane: A Plane in which the arc lies with an origin representing the + center of the circle for the arc. + radius: A number representing the radius of the arc. + a1: A number between 0 and 2 * pi for the start angle of the arc. + a2: A number between 0 and 2 * pi for the end angle of the arc. + + Properties: + * plane + * radius + * a1 + * a2 + * p1 + * p2 + * midpoint + * c + * min + * max + * length + * angle + * is_circle + * is_inverted + * arc2d + """ + __slots__ = ('_plane', '_arc2d', '_min', '_max') + + def __init__(self, plane, radius, a1=0, a2=2 * math.pi): + """Initialize Arc3D.""" + assert isinstance(plane, Plane), "Expected Plane. Got {}.".format(type(plane)) + self._plane = plane + self._arc2d = Arc2D(Point2D(0, 0), radius, a1, a2) + self._min = None + self._max = None + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Arc3D from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "type": "Arc3D" + "plane": {"n": (0, 0, 1), "o": (0, 10, 0), "x": (1, 0, 0)}, + "radius": 5, + "a1": 0, + "a2": 3.14159 + } + """ + return cls(Plane.from_dict(data['plane']), data['radius'], + data['a1'], data['a2'])
+ + +
+[docs] + @classmethod + def from_start_mid_end(cls, p1, m, p2, circle=False): + """Initialize a new arc from start, middle, and end points. + + Note that input points will be assumed to be in counterclockwise order. + + Args: + p1: The start point of the arc. + m: Any point along the length of the arc that is not the start or end. + p2: The end point of the arc. + circle: Set to True if you would like the output to be a full circle + defined by the three points instead of an arc with a start and end. + Default is False. + """ + plane = cls._plane_from_vertices(p1, m, p2) + p1_2d, m_2d, p2_2d = plane.xyz_to_xy(p1), plane.xyz_to_xy(m), plane.xyz_to_xy(p2) + arc_2d = Arc2D.from_start_mid_end(p1_2d, m_2d, p2_2d, circle) + return cls(Plane(plane.n, plane.xy_to_xyz(arc_2d.c), plane.x), + arc_2d.r, arc_2d.a1, arc_2d.a2)
+ + +
+[docs] + @classmethod + def from_arc2d(cls, arc2d, z=0): + """Initialize a new Arc3D from an Arc2D and a z value. + + Args: + arc2d: An Arc2D to be used to generate the Arc3D. + z: A number for the Z coordinate value of the arc. + """ + plane = Plane(o=Point3D(arc2d.c.x, arc2d.c.y, z)) + return cls(plane, arc2d.r, arc2d.a1, arc2d.a2)
+ + + @property + def plane(self): + """A Plane in which the arc lies with an origin for the center of the arc.""" + return self._plane + + @property + def radius(self): + """Radius of arc.""" + return self._arc2d.r + + @property + def a1(self): + """Start angle of the arc in radians.""" + return self._arc2d.a1 + + @property + def a2(self): + """End angle of the arc in radians.""" + return self._arc2d.a2 + + @property + def p1(self): + """Start point.""" + return self.plane.xy_to_xyz(self.arc2d.p1) + + @property + def p2(self): + """End point.""" + return self.plane.xy_to_xyz(self.arc2d.p2) + + @property + def midpoint(self): + """Midpoint.""" + return self.point_at(0.5) + + @property + def c(self): + """Center point of the circle on which the arc lies.""" + return self.plane.o + + @property + def min(self): + """A Point3D for the minimum bounding box vertex around this geometry.""" + if self._min is None: + self._calculate_min_max() + return self._min + + @property + def max(self): + """A Point3D for the maximum bounding box vertex around this geometry.""" + if self._max is None: + self._calculate_min_max() + return self._max + + @property + def length(self): + """The length of the arc.""" + return self.angle * self.radius + + @property + def angle(self): + """The total angle over the domain of the arc in radians.""" + _diff = self.a2 - self.a1 + return _diff if not self.is_inverted else 2 * math.pi + _diff + + @property + def area(self): + """Area of the circle to which the arc belongs.""" + assert self.is_circle, 'Arc must be a closed circle to access "area" property.' + return math.pi * self.radius ** 2 + + @property + def is_circle(self): + """Boolean for whether the arc is a full circle (True) or not (False).""" + return self.a1 == 0 and self.a2 == 2 * math.pi + + @property + def is_inverted(self): + """Boolean noting whether the end angle a2 is smaller than the start angle a1.""" + return self.a2 < self.a1 + + @property + def arc2d(self): + """An Arc2D within the plane of the Arc3D.""" + return self._arc2d + +
+[docs] + def move(self, moving_vec): + """Get an arc that has been moved along a vector. + + Args: + moving_vec: A Vector3D with the direction and distance to move the arc. + """ + return Arc3D(self.plane.move(moving_vec), self.radius, self.a1, self.a2)
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate this arc by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + return Arc3D(self.plane.rotate(axis, angle, origin), + self.radius, self.a1, self.a2)
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a arc that is rotated counterclockwise in the world XY plane by a certain angle. + + Args: + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the arc will be rotated. + """ + return Arc3D(self.plane.rotate_xy(angle, origin), self.radius, self.a1, self.a2)
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a arc reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the arc will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin from which to reflect. + """ + arc2d = self.arc2d.reflect(Vector2D(0, 1), Point2D(0, 0)) + return Arc3D(self.plane.reflect(normal, origin), self.radius, arc2d.a1, arc2d.a2)
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a arc by a factor from an origin point. + + Args: + factor: A number representing how much the arc should be scaled. + origin: A Point2D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + return Arc3D(self.plane.scale(factor, origin), self.radius * factor, + self.a1, self.a2)
+ + +
+[docs] + def subdivide(self, distances): + """Get Point3D values along the arc that subdivide it based on input distances. + + Args: + distances: A list of distances along the arc at which to subdivide it. + This can also be a single number that will be repeated until the + end of the arc. + """ + return [self.plane.xy_to_xyz(pt) for pt in self.arc2d.subdivide(distances)]
+ + +
+[docs] + def subdivide_evenly(self, number): + """Get Point3D values along the arc that divide it into evenly-spaced segments. + + Args: + number: The number of segments into which the arc will be divided. + """ + return [self.plane.xy_to_xyz(pt) for pt in self.arc2d.subdivide_evenly(number)]
+ + +
+[docs] + def point_at(self, parameter): + """Get a point at a given fraction along the arc. + + Args: + parameter: The fraction between the start and end point where the + desired point lies. For example, 0.5 will yield the midpoint. + """ + return self.plane.xy_to_xyz(self.arc2d.point_at(parameter))
+ + +
+[docs] + def point_at_angle(self, angle): + """Get a point at a given angle along the arc. + + Args: + angle: The angle in radians from the start point along the arc + to get the Point3D. + """ + return self.plane.xy_to_xyz(self.arc2d.point_at_angle(angle))
+ + +
+[docs] + def point_at_length(self, length): + """Get a point at a given distance along the arc segment. + + Args: + length: The distance along the arc from the start point where the + desired point lies. + """ + return self.point_at(length / self.length)
+ + +
+[docs] + def closest_point(self, point): + """Get the closest Point3D on this object to another Point3D. + + Args: + point: A Point3D object to which the closest point on this object + will be computed. + + Returns: + Point3D for the closest point on this line to the input point. + """ + plane_pt = self.plane.closest_point(point) + return self.plane.xy_to_xyz( + self.arc2d.closest_point(self.plane.xyz_to_xy(plane_pt)))
+ + +
+[docs] + def distance_to_point(self, point): + """Get the minimum distance between this object and the input point. + + Args: + point: A Point3D object to which the minimum distance will be computed. + + Returns: + The distance to the input point. + """ + close_pt = self.closest_point(point) + return point.distance_to_point(close_pt)
+ + +
+[docs] + def intersect_plane(self, plane): + """Get the intersection between this Arc3D and a Plane. + + Args: + plane: A Plane that will be intersected with this arc. + + Returns: + A list of Point3D objects if the intersection was successful. + None if no intersection exists. + """ + _plane_int_ray = plane.intersect_plane(self.plane) + if _plane_int_ray is not None: + _p12d = self.plane.xyz_to_xy(_plane_int_ray.p) + _p22d = self.plane.xyz_to_xy(_plane_int_ray.p + _plane_int_ray.v) + _v2d = _p22d - _p12d + _int_ray2d = Ray2D(_p12d, _v2d) + _int_pt2d = self.arc2d.intersect_line_infinite(_int_ray2d) + if _int_pt2d is not None: + return [self.plane.xy_to_xyz(pt) for pt in _int_pt2d] + return None
+ + +
+[docs] + def split_with_plane(self, plane): + """Split this Arc3D in 2 or 3 smaller arcs using a Plane. + + Args: + plane: A Plane that will be used to split this arc. + + Returns: + A list with two or three Arc3D objects if the split was successful. + Will be a list with 1 Arc3D if no intersection exists. + """ + _plane_int_ray = plane.intersect_plane(self.plane) + if _plane_int_ray is not None: + _p12d = self.plane.xyz_to_xy(_plane_int_ray.p) + _p22d = self.plane.xyz_to_xy(_plane_int_ray.p + _plane_int_ray.v) + _v2d = _p22d - _p12d + _int_ray2d = Ray2D(_p12d, _v2d) + _int_pt2d = self.arc2d.split_line_infinite(_int_ray2d) + if len(_int_pt2d) != 1: + return [Arc3D(self.plane, self.radius, arc.a1, arc.a2) + for arc in _int_pt2d] + return [self]
+ + +
+[docs] + def to_polyline(self, divisions, interpolated=True): + """Get this Arc3D as an approximated Polyline3D. + + Args: + divisions: The number of segments into which the arc will be divided. + interpolated: Boolean to note whether the polyline should be interpolated + between the input vertices when it is translated to other interfaces. + This property has no effect on the geometric calculations performed + by this library and is only present in order to assist with + display/translation. (Default: True) + """ + pts = self.subdivide_evenly(divisions) + return Polyline3D(pts, interpolated)
+ + +
+[docs] + def to_dict(self): + """Get Arc3D as a dictionary.""" + return {'type': 'Arc3D', 'plane': self.plane.to_dict(), + 'radius': self.radius, 'a1': self.a1, 'a2': self.a2}
+ + +
+[docs] + def duplicate(self): + """Get a copy of this object.""" + return self.__copy__()
+ + + def _calculate_min_max(self): + """Calculate maximum and minimum Point3D for this object. + + At this point in time, this method only calculates the bounding box + around the circle of the arc using the formula here. + https://stackoverflow.com/questions/2592011/\ + bounding-boxes-for-circle-and-arcs-in-3d + """ + if self.is_circle: + o, r = self._plane.o, self.radius + ax = self._plane.n.angle(Vector3D(1, 0, 0)) + ay = self._plane.n.angle(Vector3D(0, 1, 0)) + az = self._plane.n.angle(Vector3D(0, 0, 1)) + r_vec = (math.sin(ax) * r, math.sin(ay) * r, math.sin(az) * r) + self._min = Point3D(o.x - r_vec[0], o.y - r_vec[1], o.z - r_vec[2]) + self._max = Point3D(o.x + r_vec[0], o.y + r_vec[1], o.z + r_vec[2]) + else: # get the min and max of the Arc2D + min_pt2d, max_pt2d = self._arc2d.min, self._arc2d.max + self._min = self.plane.xy_to_xyz(min_pt2d) + self._max = self.plane.xy_to_xyz(max_pt2d) + + @staticmethod + def _plane_from_vertices(pt1, pt2, pt3): + """Get a plane from three vertices.""" + try: + v1 = pt2 - pt1 + v2 = pt3 - pt1 + n = v1.cross(v2) + except Exception as e: + raise ValueError('Incorrect Point3D input for Arc3D:\n\t{}'.format(e)) + return Plane(n, pt1) + + def __copy__(self): + return Arc3D(self.plane, self.radius, self.a1, self.a2) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (hash(self.plane), self.radius, self.a1, self.a2) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Arc3D) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + +
+[docs] + def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__()
+ + + def __repr__(self): + return 'Arc3D (center {}) (radius {}) (length {})'.format( + self.plane.o, self.radius, self.length)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/cone.html b/docs/_modules/ladybug_geometry/geometry3d/cone.html new file mode 100644 index 00000000..fc500378 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/cone.html @@ -0,0 +1,1418 @@ + + + + + + + ladybug_geometry.geometry3d.cone — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.cone

+# coding=utf-8
+"""Cone"""
+from __future__ import division
+
+from .pointvector import Point3D, Vector3D
+from .plane import Plane
+from .arc import Arc3D
+
+import math
+
+
+
+[docs] +class Cone(object): + """Cone object. + + Args: + vertex: A Point3D at the tip of the cone. + axis: A Vector3D representing the direction and height of the cone. + The vector extends from the vertex to the center of the base. + angle: An angle in radians representing the half angle between + the axis and the surface. + + Properties: + * vertex + * axis + * angle + * height + * slant_height + * radius + * area + * volume + * min + * max + * base + """ + __slots__ = ('_vertex', '_axis', '_angle', '_base', '_min', '_max') + + def __init__(self, vertex, axis, angle): + """Initialize Cone.""" + assert isinstance(vertex, Point3D), \ + "Expected Point3D. Got {}.".format(type(vertex)) + assert isinstance(axis, Vector3D), \ + "Expected Vector3D. Got {}.".format(type(axis)) + assert angle > 0, 'Cone angle must be greater than 0. Got {}.'.format(angle) + self._vertex = vertex + self._axis = axis + self._angle = angle + self._base = None + self._min = None + self._max = None + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Cone from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "type": "Cone" + "vertex": (10, 0, 0), + "axis": (0, 0, 1), + "angle": 1.0 + } + """ + return cls(Point3D.from_array(data['vertex']), + Vector3D.from_array(data['axis']), + data['angle'])
+ + + @property + def vertex(self): + """Vertex of cone.""" + return self._vertex + + @property + def axis(self): + """Axis of cone.""" + return self._axis + + @property + def angle(self): + """Angle of cone""" + return self._angle + + @property + def height(self): + """Height of cone""" + return self.axis.magnitude + + @property + def radius(self): + """Radius of a cone""" + return self.height * math.tan(self.angle) + + @property + def slant_height(self): + """Slant height of a cone""" + return math.sqrt(self.radius ** 2 + self.height ** 2) + + @property + def area(self): + """Surface area of a cone""" + return math.pi * self.radius ** 2 + math.pi * self.radius * self.slant_height + + @property + def volume(self): + """Volume of a cone""" + return 1 / 3 * math.pi * self.radius ** 2 * self.height + + @property + def base(self): + """Get an Arc3D representing the circular base of the cone.""" + if self._base is None: + plane = Plane(self.axis.reverse(), self.vertex + self.axis) + self._base = Arc3D(plane, self.radius) + return self._base + + @property + def min(self): + """A Point3D for the minimum bounding box vertex around this geometry.""" + if self._min is None: + self._calculate_min_max() + return self._min + + @property + def max(self): + """A Point3D for the maximum bounding box vertex around this geometry.""" + if self._max is None: + self._calculate_min_max() + return self._max + +
+[docs] + def move(self, moving_vec): + """Get a cone that has been moved along a vector. + + Args: + moving_vec: A Vector3D with the direction and distance to move the cone. + """ + return Cone(self.vertex.move(moving_vec), self.axis, self.angle)
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate this cone by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the cone will be rotated. + """ + return Cone(self.vertex.rotate(axis, angle, origin), + self.axis.rotate(axis, angle), + self.angle)
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a cone that is rotated counterclockwise in the world XY plane by an angle. + + Args: + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the cone will be rotated. + """ + return Cone(self.vertex.rotate_xy(angle, origin), + self.axis.rotate_xy(angle), + self.angle)
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a cone reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the arc will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin from which to reflect. + """ + return Cone(self.vertex.reflect(normal, origin), + self.axis.reflect(normal), + self.angle)
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a cone by a factor from an origin point. + + Args: + factor: A number representing how much the cone should be scaled. + origin: A Point3D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + return Cone(self.vertex.scale(factor, origin), + self.axis * factor, + self.angle)
+ + +
+[docs] + def duplicate(self): + """Get a copy of this object.""" + return self.__copy__()
+ + +
+[docs] + def to_dict(self): + """Get Cone as a dictionary.""" + return { + 'type': 'Cone', + 'vertex': self.vertex.to_array(), + 'axis': self.axis.to_array(), + 'angle': self.angle + }
+ + + def _calculate_min_max(self): + """Calculate maximum and minimum Point3D for this object.""" + base = self.base + bmn, bmx, ver = base.min, base.max, self.vertex + self._min = Point3D(min(bmn.x, ver.x), min(bmn.y, ver.y), min(bmn.z, ver.z)) + self._max = Point3D(max(bmx.x, ver.x), max(bmx.y, ver.y), max(bmx.z, ver.z)) + + def __copy__(self): + return Cone(self.vertex, self.axis, self.angle) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self._vertex, self._axis, self._angle) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Cone) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + +
+[docs] + def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__()
+ + + def __repr__(self): + return 'Cone (vertex {}) (axis {}) (angle {}) (height {})'.\ + format(self.vertex, self.axis, self.angle, self.height)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/cylinder.html b/docs/_modules/ladybug_geometry/geometry3d/cylinder.html new file mode 100644 index 00000000..29f5adb5 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/cylinder.html @@ -0,0 +1,1447 @@ + + + + + + + ladybug_geometry.geometry3d.cylinder — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.cylinder

+# coding=utf-8
+"""Cylinder"""
+from __future__ import division
+
+from .pointvector import Point3D, Vector3D
+from .plane import Plane
+from .arc import Arc3D
+
+import math
+
+
+
+[docs] +class Cylinder(object): + """Cylinder object. + + Args: + center: A Point3D at the center of the bottom base of the cylinder. + axis: A Vector3D representing the direction and height of the cylinder. + The vector extends from the bottom base center to the top base center. + radius: A number representing the radius of the cylinder. + + Properties: + * center + * axis + * radius + * center_end + * diameter + * height + * area + * volume + * min + * max + * base_bottom + * base_top + """ + __slots__ = ('_center', '_axis', '_radius', + '_base_bottom', '_base_top', '_min', '_max') + + def __init__(self, center, axis, radius): + """Initialize Cylinder.""" + assert isinstance(center, Point3D), \ + "Expected Point3D. Got {}.".format(type(center)) + assert isinstance(axis, Vector3D), \ + "Expected Vector3D. Got {}.".format(type(axis)) + assert radius > 0, \ + 'Cylinder radius must be greater than 0. Got {}.'.format(radius) + self._center = center + self._axis = axis + self._radius = radius + self._base_bottom = None + self._base_top = None + self._min = None + self._max = None + +
+[docs] + @classmethod + def from_start_end(cls, p1, p2, radius): + """Initialize a new cylinder from start and end points. + + Args: + p1: The start point of the cylinder, represents the center of the + bottom base of the cylinder. + p2: The end point of the cylinder, represents the center of the top + base of the cylinder + radius: A number representing the radius of the cylinder. + """ + axis = p2 - p1 + return cls(p1, axis, radius)
+ + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Cylinder from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "type": "Cylinder" + "center": (10, 0, 0), + "axis": (0, 0, 1), + "radius": 1.0 + } + """ + return cls(Point3D.from_array(data['center']), + Vector3D.from_array(data['axis']), + data['radius'])
+ + + @property + def center(self): + """Center of Cylinder.""" + return self._center + + @property + def axis(self): + """Axis of Cylinder.""" + return self._axis + + @property + def radius(self): + """Radius of Cylinder""" + return self._radius + + @property + def center_end(self): + """Center of the opposite end of Cylinder.""" + return self.center + self.axis + + @property + def diameter(self): + """Diameter of Cylinder""" + return self.radius * 2 + + @property + def height(self): + """Height of Cylinder""" + return self.axis.magnitude + + @property + def area(self): + """Surface area of a Cylinder""" + return 2 * math.pi * self.radius * self.height + 2 * math.pi * self.radius ** 2 + + @property + def volume(self): + """Volume of a Cylinder""" + return math.pi * self.radius ** 2 * self.height + + @property + def base_bottom(self): + """Get an Arc3D representing the bottom circular base of the cylinder.""" + if self._base_bottom is None: + self._base_bottom = Arc3D(Plane(self.axis, self.center), self.radius) + return self._base_bottom + + @property + def base_top(self): + """Get an Arc3D representing the top circular base of the cylinder.""" + if self._base_top is None: + plane = Plane(self.axis, self.center + self.axis) + self._base_top = Arc3D(plane, self.radius) + return self._base_top + + @property + def min(self): + """A Point3D for the minimum bounding box vertex around this geometry.""" + if self._min is None: + self._calculate_min_max() + return self._min + + @property + def max(self): + """A Point3D for the maximum bounding box vertex around this geometry.""" + if self._max is None: + self._calculate_min_max() + return self._max + +
+[docs] + def move(self, moving_vec): + """Get a Cylinder that has been moved along a vector. + + Args: + moving_vec: A Vector3D with the direction and distance to move the Cylinder. + """ + return Cylinder(self.center.move(moving_vec), self.axis, self.radius)
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate this Cylinder by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the cylinder will be rotated. + """ + return Cylinder(self.center.rotate(axis, angle, origin), + self.axis.rotate(axis, angle), + self.radius)
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a Cylinder that is rotated counterclockwise in the world XY plane by an angle. + + Args: + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the cylinder will be rotated. + """ + return Cylinder(self.center.rotate_xy(angle, origin), + self.axis.rotate_xy(angle), + self.radius)
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a Cylinder reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the arc will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin from which to reflect. + """ + return Cylinder(self.center.reflect(normal, origin), + self.axis.reflect(normal), + self.radius)
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a Cylinder by a factor from an origin point. + + Args: + factor: A number representing how much the Cylinder should be scaled. + origin: A Point3D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + return Cylinder(self.center.scale(factor, origin), + self.axis * factor, + self.radius * factor)
+ + +
+[docs] + def duplicate(self): + """Get a copy of this object.""" + return self.__copy__()
+ + +
+[docs] + def to_dict(self): + """Get Cylinder as a dictionary.""" + return { + 'type': 'Cylinder', + 'center': self.center.to_array(), + 'axis': self.axis.to_array(), + 'radius': self.radius + }
+ + + def _calculate_min_max(self): + """Calculate maximum and minimum Point3D for this object.""" + base1, base2 = self.base_bottom, self.base_top + b1mn, b1mx, b2mn, b2mx = base1.min, base1.max, base2.min, base2.max + self._min = Point3D( + min(b1mn.x, b2mn.x), min(b1mn.y, b2mn.y), min(b1mn.z, b2mn.z)) + self._max = Point3D( + max(b1mx.x, b2mx.x), max(b1mx.y, b2mx.y), max(b1mx.z, b2mx.z)) + + def __copy__(self): + return Cylinder(self.center, self.axis, self.radius) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self._center, self._axis, self._radius) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Cylinder) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + +
+[docs] + def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__()
+ + + def __repr__(self): + return 'Cylinder (center {}) (axis {}) (radius {})'.\ + format(self.center, self.axis, self.radius)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/face.html b/docs/_modules/ladybug_geometry/geometry3d/face.html new file mode 100644 index 00000000..1595ce9b --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/face.html @@ -0,0 +1,4579 @@ + + + + + + + ladybug_geometry.geometry3d.face — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.face

+# coding=utf-8
+"""Planar Face in 3D Space"""
+from __future__ import division
+import math
+import sys
+if (sys.version_info > (3, 0)):  # python 3
+    xrange = range
+
+from .pointvector import Point3D, Vector3D
+from .ray import Ray3D
+from .line import LineSegment3D
+from .polyline import Polyline3D
+from .plane import Plane
+from .mesh import Mesh3D
+from ._2d import Base2DIn3D
+
+from ..intersection3d import closest_point3d_on_line3d
+from ..network import DirectedGraphNetwork
+
+from ..geometry2d.pointvector import Point2D, Vector2D
+from ..geometry2d.ray import Ray2D
+from ..geometry2d.line import LineSegment2D
+from ..geometry2d.polyline import Polyline2D
+from ..geometry2d.polygon import Polygon2D
+from ..geometry2d.mesh import Mesh2D
+
+import ladybug_geometry.boolean as pb
+
+
+
+[docs] +class Face3D(Base2DIn3D): + """Planar Face in 3D space. + + Args: + boundary: A list or tuple of Point3D objects representing the outer + boundary vertices of the face. + plane: A Plane object indicating the plane in which the face exists. + If None, the Plane normal will automatically be calculated by + analyzing the input vertices and the origin of the plane will be + the first vertex of the input vertices. Default: None. + holes: Optional list of lists with one list for each hole in the face. + Each hole should be a list of at least 3 Point3D objects. + If None, it will be assumed that there are no holes in the face. + The boundary and holes are stored as separate lists of Point3Ds on the + `boundary` and `holes` properties of this object. However, the + `vertices` property will always contain all vertices across the shape. + For a Face3D that has holes, it will trace out a single shape that + turns inwards from the boundary to cut out the holes. + enforce_right_hand: Boolean to note whether a check should be run to + ensure that input vertices are counterclockwise within the input plane, + thereby enforcing the right-hand rule. By default, this is True + and ensures that all Face3D objects adhere to the right-hand rule. + It is recommended that this only be set to False in cases where you + are certain that the input vertices are counter-clockwise + within the input plane and you would like to avoid the extra + unnecessary check. + + Properties: + * vertices + * plane + * boundary + * holes + * polygon2d + * boundary_polygon2d + * hole_polygon2d + * triangulated_mesh2d + * triangulated_mesh3d + * boundary_segments + * hole_segments + * normal + * min + * max + * center + * perimeter + * area + * centroid + * azimuth + * altitude + * tilt + * is_clockwise + * is_convex + * is_self_intersecting + * self_intersection_points + * is_valid + * has_holes + * upper_left_corner + * lower_left_corner + * upper_right_corner + * lower_right_corner + * upper_left_counter_clockwise_vertices + * lower_left_counter_clockwise_vertices + * lower_right_counter_clockwise_vertices + * upper_right_counter_clockwise_boundary + * upper_left_counter_clockwise_boundary + * lower_left_counter_clockwise_boundary + * lower_right_counter_clockwise_boundary + * upper_right_counter_clockwise_boundary + """ + __slots__ = ('_plane', '_polygon2d', '_mesh2d', '_mesh3d', + '_boundary', '_holes', '_boundary_segments', '_hole_segments', + '_boundary_polygon2d', '_hole_polygon2d', + '_perimeter', '_area', '_centroid', + '_is_convex', '_is_self_intersecting') + HOLE_VERTEX_THRESHOLD = 400 # threshold at which faster hole merging method is used + + def __init__(self, boundary, plane=None, holes=None, enforce_right_hand=True): + """Initialize Face3D.""" + # process the boundary and plane inputs + self._boundary = self._check_vertices_input(boundary) + if plane is not None: + assert isinstance(plane, Plane), 'Expected Plane for Face3D.' \ + ' Got {}.'.format(type(plane)) + else: + plane = self._plane_from_vertices(boundary) + self._plane = plane + + # process boundary and holes input + if holes: + assert isinstance(holes, (tuple, list)), \ + 'holes should be a tuple or list. Got {}'.format(type(holes)) + self._holes = tuple( + self._check_vertices_input(hole, 'hole') for hole in holes) + # create a Polygon2D from the vertices + _boundary2d = [self._plane.xyz_to_xy(_v) for _v in boundary] + _holes2d = [[self._plane.xyz_to_xy(_v) for _v in hole] for hole in holes] + v_count = len(_boundary2d) # count the vertices for hole merging method + for h in _holes2d: + v_count += len(h) + _polygon2d = Polygon2D.from_shape_with_holes_fast(_boundary2d, _holes2d) \ + if v_count > self.HOLE_VERTEX_THRESHOLD else \ + Polygon2D.from_shape_with_holes(_boundary2d, _holes2d) + # convert Polygon2D vertices to 3D to become the vertices of the face. + self._vertices = tuple(self._plane.xy_to_xyz(_v) + for _v in _polygon2d.vertices) + self._polygon2d = _polygon2d + else: + self._holes = None + self._vertices = self._boundary + self._polygon2d = None + + # perform a check of vertex orientation and enforce counter clockwise vertices + if enforce_right_hand is True: + if self.is_clockwise is True: + self._boundary = tuple(reversed(self._boundary)) + self._vertices = tuple(reversed(self._vertices)) + if self._polygon2d is not None: + self._polygon2d = self._polygon2d.reverse() + + # set other properties to None for now + self._mesh2d = None + self._mesh3d = None + self._boundary_polygon2d = None + self._hole_polygon2d = None + self._boundary_segments = None + self._hole_segments = None + self._min = None + self._max = None + self._center = None + self._perimeter = None + self._area = None + self._centroid = None + self._is_convex = None + self._is_self_intersecting = None + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Face3D from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "type": "Face3D", + "boundary": [(0, 0, 0), (10, 0, 0), (0, 10, 0)], + "plane": {"n": (0, 0, 1), "o": (0, 0, 0), "x": (1, 0, 0)}, + "holes": [[(2, 2, 0), (5, 2, 0), (2, 5, 0)]] + } + """ + holes = None + if 'holes' in data and data['holes'] is not None: + holes = tuple(tuple( + Point3D.from_array(pt) for pt in hole) for hole in data['holes']) + plane = None + if 'plane' in data and data['plane'] is not None: + plane = Plane.from_dict(data['plane']) + return cls(tuple(Point3D.from_array(pt) for pt in data['boundary']), + plane, holes)
+ + +
+[docs] + @classmethod + def from_array(cls, point_array): + """Create a Face3D from a nested array of vertex coordinates. + + Args: + point_array: A nested array of arrays where each sub-array represents + a loop of the Face3D. The first array is the boundary and subsequent + arrays represent holes in the Face3D. point arrays. Each sub-array + is composed of arrays that each have a length of 3 and denote 3D + points that define the face. + """ + boundary = tuple(Point3D(*point) for point in point_array[0]) + holes = None if len(point_array) == 1 else \ + tuple(tuple(Point3D(*point) for point in hole) for hole in point_array[1:]) + return cls(boundary, None, holes)
+ + +
+[docs] + @classmethod + def from_extrusion(cls, line_segment, extrusion_vector): + """Initialize Face3D by extruding a line segment. + + Initializing a face this way has the added benefit of having its + properties quickly computed. + + Args: + line_segment: A LineSegment3D to be extruded. + extrusion_vector: A vector denoting the direction and distance to + extrude the line segment. + """ + assert isinstance(line_segment, LineSegment3D), \ + 'line_segment must be LineSegment3D. Got {}.'.format(type(line_segment)) + assert isinstance(extrusion_vector, Vector3D), \ + 'extrusion_vector must be Vector3D. Got {}.'.format(type(extrusion_vector)) + _p1 = line_segment.p1 + _p2 = line_segment.p2 + _verts = (_p1, _p2, _p2 + extrusion_vector, _p1 + extrusion_vector) + _plane = Plane(line_segment.v.cross(extrusion_vector), _p1) + face = cls(_verts, _plane, enforce_right_hand=False) + _base = line_segment.length + _dist = extrusion_vector.magnitude + _height = _dist * math.sin(extrusion_vector.angle(line_segment.v)) + face._perimeter = _base * 2 + _dist * 2 + face._area = _base * _height + face._centroid = _p1 + (line_segment.v * 0.5) + (extrusion_vector * 0.5) + face._is_convex = True + face._is_self_intersecting = False + return face
+ + +
+[docs] + @classmethod + def from_rectangle(cls, base, height, base_plane=None): + """Initialize Face3D from rectangle parameters (base + height) and a base plane. + + Initializing a face this way has the added benefit of having its + properties quickly computed. + + Args: + base: A number indicating the length of the base of the rectangle. + height: A number indicating the length of the height of the rectangle. + base_plane: A Plane object in which the rectangle will be created. + The origin of this plane will be the lower left corner of the + rectangle and the X and Y axes will form the sides. + Default is the world XY plane. + """ + assert isinstance(base, (float, int)), 'Rectangle base must be a number.' + assert isinstance(height, (float, int)), 'Rectangle height must be a number.' + if base_plane is not None: + assert isinstance(base_plane, Plane), \ + 'base_plane must be Plane. Got {}.'.format(type(base_plane)) + else: + base_plane = Plane(Vector3D(0, 0, 1), Point3D(0, 0, 0)) + _o = base_plane.o + _b_vec = base_plane.x * base + _h_vec = base_plane.y * height + _verts = (_o, _o + _b_vec, _o + _h_vec + _b_vec, _o + _h_vec) + face = cls(_verts, base_plane, enforce_right_hand=False) + face._perimeter = base * 2 + height * 2 + face._area = base * height + face._centroid = _o + (_b_vec * 0.5) + (_h_vec * 0.5) + face._is_convex = True + face._is_self_intersecting = False + return face
+ + +
+[docs] + @classmethod + def from_regular_polygon(cls, side_count, radius=1, base_plane=None): + """Initialize Face3D from regular polygon parameters and a base_plane. + + Args: + side_count: An integer for the number of sides on the regular + polygon. This number must be greater than 2. + radius: A number indicating the distance from the polygon's center + where the vertices of the polygon will lie. + The default is set to 1. + base_plane: A Plane object for the plane in which the face exists. + The origin of this plane will be used as the center of the polygon. + If None, the default will be the WorldXY plane. + """ + # set the default base_plane + if base_plane is not None: + assert isinstance(base_plane, Plane), 'Expected Plane. Got {}'.format( + type(base_plane)) + else: + base_plane = Plane(Vector3D(0, 0, 1), Point3D(0, 0, 0)) + + # create the regular polygon face + _polygon2d = Polygon2D.from_regular_polygon(side_count, radius) + _vert3d = tuple(base_plane.xy_to_xyz(_v) for _v in _polygon2d.vertices) + _face = cls(_vert3d, base_plane, enforce_right_hand=False) + + # assign extra properties that we know to the face + _face._polygon2d = _polygon2d + _face._center = base_plane.o + _face._centroid = base_plane.o + _face._is_convex = True + _face._is_self_intersecting = False + return _face
+ + +
+[docs] + @classmethod + def from_punched_geometry(cls, base_face, sub_faces): + """Create a face with holes punched in it from sub-faces. + + Args: + base_face: A Face3D that acts as a parent to the sub_faces, completely + encircling them. + sub_faces: A list of Face3D objects that will be punched into the + base_face. These faces must lie completely within the base_face + for the result to be valid. The is_sub_face() method can be + used to check sub_faces before they are input here. + """ + assert isinstance(base_face, Face3D), \ + 'base_face should be a Face3D. Got {}'.format(type(base_face)) + for hole in sub_faces: + assert isinstance(hole, Face3D), \ + 'sub_face should be a list. Got {}'.format(type(hole)) + hole_verts = [list(sf.boundary) for sf in sub_faces] + if base_face.has_holes: + hole_verts.extend([list(h) for h in base_face.holes]) + return cls(base_face.boundary, base_face.plane, hole_verts, + enforce_right_hand=False)
+ + + @property + def vertices(self): + """Tuple of all vertices in this face. + + Note that, in the case of a face with holes, some vertices will be repeated + since this property effectively traces out a single boundary around the + whole shape, winding inward to cut out the holes. + """ + return self._vertices + + @property + def plane(self): + """Tuple of all vertices in this face.""" + return self._plane + + @property + def polygon2d(self): + """A Polygon2D of this face in the 2D space of the face's plane. + + Note that this is a single polygon object even when there are holes in the + face since such a polygon can be made by drawing a line from the holes to + the outer boundary. + """ + if self._polygon2d is None: + _vert2d = tuple(self._plane.xyz_to_xy(_v) for _v in self.vertices) + self._polygon2d = Polygon2D(_vert2d) + return self._polygon2d + + @property + def triangulated_mesh2d(self): + """A triangulated Mesh2D in the 2D space of the face's plane.""" + if self._mesh2d is None: + self._mesh2d = Mesh2D.from_polygon_triangulated( + self.boundary_polygon2d, self.hole_polygon2d) + return self._mesh2d + + @property + def triangulated_mesh3d(self): + """A triangulated Mesh3D of this face.""" + if self._mesh3d is None: + _vert3d = tuple(self._plane.xy_to_xyz(_v) for _v in + self.triangulated_mesh2d.vertices) + self._mesh3d = Mesh3D(_vert3d, self.triangulated_mesh2d.faces) + return self._mesh3d + + @property + def boundary(self): + """Tuple of vertices on the boundary of this face. + + For most Face3D objects, this will be identical to the vertices property. + However, when the Face3D has holes within it, this property stores + the outer boundary of the shape. + """ + return self._boundary + + @property + def holes(self): + """Tuple with one tuple of vertices for each hole within this face. + + This property will be None when the face has no holes in it. + """ + return self._holes + + @property + def boundary_segments(self): + """Tuple of all line segments bordering the face. + + Note that this does not include segments for any holes in the face. + Just the outer boundary. + """ + if self._boundary_segments is None: + _segs = [] + for i, vert in enumerate(self.boundary): + _seg = LineSegment3D.from_end_points(self.boundary[i - 1], vert) + _segs.append(_seg) + _segs.append(_segs.pop(0)) # segments will start from the first vertex + self._boundary_segments = tuple(_segs) + return self._boundary_segments + + @property + def hole_segments(self): + """Tuple with a tuple of line segments for each hole in the face. + + This will be None if there are no holes in the face. + """ + if self._holes is not None and self._hole_segments is None: + _all_segs = [] + for hole in self.holes: + _segs = [] + for i, vert in enumerate(hole): + _seg = LineSegment3D.from_end_points(hole[i - 1], vert) + _segs.append(_seg) + _segs.append(_segs.pop(0)) # segments will start from the first vertex + _all_segs.append(_segs) + self._hole_segments = tuple(tuple(_s) for _s in _all_segs) + return self._hole_segments + + @property + def boundary_polygon2d(self): + """A Polygon2D of the face boundary in the 2D space of the face's plane. + + Note that this does not include any holes in the face. Just the outer boundary. + """ + if self._boundary_polygon2d is None: + _vert2d = tuple(self._plane.xyz_to_xy(_v) for _v in self.boundary) + self._boundary_polygon2d = Polygon2D(_vert2d) + return self._boundary_polygon2d + + @property + def hole_polygon2d(self): + """A list of Polygon2D for the face holes in the 2D space of the face's plane. + """ + if self._holes is not None and self._hole_polygon2d is None: + self._hole_polygon2d = [] + for hole in self.holes: + _vert2d = tuple(self._plane.xyz_to_xy(_v) for _v in hole) + self._hole_polygon2d.append(Polygon2D(_vert2d)) + return self._hole_polygon2d + + @property + def normal(self): + """Normal vector for the plane in which the face exists.""" + return self._plane.n + + @property + def perimeter(self): + """The perimeter of the face. This includes the length of holes in the face.""" + if self._perimeter is None: + self._perimeter = sum([seg.length for seg in self.boundary_segments]) + if self._holes is not None: + for hole in self.hole_segments: + self._perimeter += sum([seg.length for seg in hole]) + return self._perimeter + + @property + def area(self): + """The area of the face.""" + if self._area is None: + self._area = self.polygon2d.area + return self._area + + @property + def centroid(self): + """The centroid of the face as a Point3D (aka. center of mass). + + Note that the centroid is more time consuming to compute than the center + (or the middle point of the face bounding box). So the center might be + preferred over the centroid if you just need a rough point for the middle + of the face. + """ + if self._centroid is None: + _cent2d = self.triangulated_mesh2d.centroid + self._centroid = self._plane.xy_to_xyz(_cent2d) + return self._centroid + + @property + def azimuth(self): + """Get the azimuth of the Face3D (between 0 and 2 * Pi). + + This will be zero if the Face3D is perfectly horizontal. + """ + return self.plane.azimuth + + @property + def altitude(self): + """Get the altitude of the Face3D. Between Pi/2 (up) and -Pi/2 (down).""" + return self.plane.altitude + + @property + def tilt(self): + """Get the tilt of the Face3D. Between 0 (up) and Pi (down).""" + return self.plane.tilt + + @property + def is_clockwise(self): + """Boolean for whether the face vertices and boundary are in clockwise order. + + Note that all Face3D objects should have counterclockwise vertices (meaning + that this property should always be False). This property exists largely + for testing / debugging purposes. + """ + return self.polygon2d.is_clockwise + + @property + def is_convex(self): + """Boolean noting whether the face is convex (True) or non-convex (False). + + Note that any face with holes will be automatically considered non-convex + since the underlying polygon_2d is always non-convex in this case. + """ + if self._is_convex is None: + self._is_convex = self.polygon2d.is_convex + return self._is_convex + + @property + def is_self_intersecting(self): + """Boolean noting whether the face has self-intersecting edges. + + Note that this property is relatively computationally intense to obtain compared + to properties like area and is_convex. Also, most CAD programs forbid geometry + with self-intersecting edges. So it is recommended that this property only + be used in quality control scripts where the origin of the geometry is unknown. + """ + if self._is_self_intersecting is None: + self._is_self_intersecting = False + if self.boundary_polygon2d.is_self_intersecting: + self._is_self_intersecting = True + if self.has_holes: + for hp in self.hole_polygon2d: + if hp.is_self_intersecting: + self._is_self_intersecting = True + break + return self._is_self_intersecting + + @property + def self_intersection_points(self): + """A tuple of Point3Ds for the locations where the Face3D intersects itself. + + This will be an empty tuple if the Face3D is not self-intersecting and it + is generally recommended that the Face3D.is_self_intersecting property + be checked before using this property. + """ + if self.is_self_intersecting: + int_pts = [] + for pt2 in self.boundary_polygon2d.self_intersection_points: + int_pts.append(self.plane.xy_to_xyz(pt2)) + if self.has_holes: + for hp in self.hole_polygon2d: + for pt2 in hp.self_intersection_points: + int_pts.append(self.plane.xy_to_xyz(pt2)) + return tuple(int_pts) + return () + + @property + def is_valid(self): + """Boolean noting whether the face is valid (having a non-zero area). + + Note that faces are still considered valid if they have out-of-plane vertices, + self-intersecting edges, or duplicate/colinear vertices. The check_planar + method can be used to detect if there are out-of-plane vertices. The + is_self_intersecting property identifies self-intersecting edges, and the + remove_colinear_vertices method will remove duplicate/colinear vertices.""" + return not self.area == 0 + + @property + def has_holes(self): + """Boolean noting whether the face has holes within it.""" + return self._holes is not None + + @property + def upper_left_corner(self): + """Get the vertex in the upper-left corner of the face's bounding box.""" + return self._corner_point('min', 'max') + + @property + def lower_left_corner(self): + """Get the vertex in the lower-left corner of the face's bounding box.""" + return self._corner_point('min', 'min') + + @property + def upper_right_corner(self): + """Get the vertex in the upper-right corner of the face's bounding box.""" + return self._corner_point('max', 'max') + + @property + def lower_right_corner(self): + """Get the vertex in the lower-right corner of the face's bounding box.""" + return self._corner_point('max', 'min') + + @property + def upper_left_counter_clockwise_vertices(self): + """Get face vertices starting from the upper left and moving counterclockwise. + + Horizontal faces will treat the positive Y axis as up. All other faces + treat the positive Z axis as up. + """ + corner_pt, polygon = self._corner_point_and_polygon(self._vertices, 'min', 'max') + verts3d, verts2d = self._counter_clockwise_verts(polygon) + return self._corner_pt_verts(corner_pt, verts3d, verts2d) + + @property + def lower_left_counter_clockwise_vertices(self): + """Get face vertices starting from the lower left and moving counterclockwise. + + Horizontal faces will treat the positive Y axis as up. All other faces + treat the positive Z axis as up. + """ + corner_pt, polygon = self._corner_point_and_polygon(self._vertices, 'min', 'min') + verts3d, verts2d = self._counter_clockwise_verts(polygon) + return self._corner_pt_verts(corner_pt, verts3d, verts2d) + + @property + def lower_right_counter_clockwise_vertices(self): + """Get face vertices starting from the lower left and moving counterclockwise. + + Horizontal faces will treat the positive Y axis as up. All other faces + treat the positive Z axis as up. + """ + corner_pt, polygon = self._corner_point_and_polygon(self._vertices, 'max', 'min') + verts3d, verts2d = self._counter_clockwise_verts(polygon) + return self._corner_pt_verts(corner_pt, verts3d, verts2d) + + @property + def upper_right_counter_clockwise_vertices(self): + """Get face vertices starting from the lower left and moving counterclockwise. + + Horizontal faces will treat the positive Y axis as up. All other faces + treat the positive Z axis as up. + """ + corner_pt, polygon = self._corner_point_and_polygon(self._vertices, 'max', 'max') + verts3d, verts2d = self._counter_clockwise_verts(polygon) + return self._corner_pt_verts(corner_pt, verts3d, verts2d) + + @property + def upper_left_counter_clockwise_boundary(self): + """Get face boundary starting from the upper left and moving counterclockwise. + + Horizontal faces will treat the positive Y axis as up. All other faces + treat the positive Z axis as up. + """ + corner_pt, polygon = self._corner_point_and_polygon(self._boundary, 'min', 'max') + verts3d, verts2d = self._counter_clockwise_bound(polygon) + return self._corner_pt_verts(corner_pt, verts3d, verts2d) + + @property + def lower_left_counter_clockwise_boundary(self): + """Get face boundary starting from the lower left and moving counterclockwise. + + Horizontal faces will treat the positive Y axis as up. All other faces + treat the positive Z axis as up. + """ + corner_pt, polygon = self._corner_point_and_polygon(self._boundary, 'min', 'min') + verts3d, verts2d = self._counter_clockwise_bound(polygon) + return self._corner_pt_verts(corner_pt, verts3d, verts2d) + + @property + def lower_right_counter_clockwise_boundary(self): + """Get face boundary starting from the lower left and moving counterclockwise. + + Horizontal faces will treat the positive Y axis as up. All other faces + treat the positive Z axis as up. + """ + corner_pt, polygon = self._corner_point_and_polygon(self._boundary, 'max', 'min') + verts3d, verts2d = self._counter_clockwise_bound(polygon) + return self._corner_pt_verts(corner_pt, verts3d, verts2d) + + @property + def upper_right_counter_clockwise_boundary(self): + """Get face boundary starting from the lower left and moving counterclockwise. + + Horizontal faces will treat the positive Y axis as up. All other faces + treat the positive Z axis as up. + """ + corner_pt, polygon = self._corner_point_and_polygon(self._boundary, 'max', 'max') + verts3d, verts2d = self._counter_clockwise_bound(polygon) + return self._corner_pt_verts(corner_pt, verts3d, verts2d) + +
+[docs] + def pole_of_inaccessibility(self, tolerance): + """Get the pole of inaccessibility for the Face3D. + + The pole of inaccessibility is the most distant internal point from the + Face3D outline. It is not to be confused with the centroid, which + represents the "center of mass" of the shape and may be outside of + the Face3D if the shape is concave. The poly of inaccessibility is + useful for optimal placement of a text label on the Face3D. + + Args: + tolerance: The precision to which the pole of inaccessibility + will be computed. + """ + return self.plane.xy_to_xyz(self.polygon2d.pole_of_inaccessibility(tolerance))
+ + +
+[docs] + def is_horizontal(self, tolerance): + """Check whether a this face is horizontal within a given tolerance. + + Args: + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. + + Returns: + True if the face is horizontal. False if it is not. + """ + return self.max.z - self.min.z <= tolerance
+ + +
+[docs] + def is_coplanar(self, face, tolerance): + """Check whether a this face is coplanar with another given tolerance. + + This method will only evaluate the distance between the other face's + vertices and this face's plane so it does not rely on a check based + on angle tolerance. + + Args: + face: A neighboring Face3D for which co-planarity will be checked. + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. + + Returns: + True if the face is coplanar with this one. False if it is not. + """ + for pt in face.vertices: + if self.plane.distance_to_point(pt) > tolerance: + return False + return True
+ + +
+[docs] + def is_geometrically_equivalent(self, face, tolerance): + """Check whether a given face is geometrically equivalent to this Face. + + Geometrical equivalence is defined as being coplanar with this face, + having the same number of vertices, and having each vertex map-able between + the faces. Clockwise relationships do not have to match nor does the normal + direction of the face. However, all other properties must be matching to + within the input tolerance. + + This is useful for identifying matching surfaces when solving for adjacency + and you need to ensure that two faces match perfectly in their area and vertices. + Note that you may also want to use the remove_colinear_vertices() method + on input faces before using this method in order to count faces with the + same non-colinear vertices as geometrically equivalent. + + Args: + face: Another face for which geometric equivalency will be tested. + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered geometrically equivalent. + + Returns: + True if geometrically equivalent. False if not geometrically equivalent. + """ + # rule out surfaces if they don't fit key criteria + if not self.center.is_equivalent(face.center, tolerance): + return False + if len(self.vertices) != len(face.vertices): + return False + + # see if we can find a matching vertex + match_i = None + for i, pt in enumerate(self.vertices): + if pt.is_equivalent(face[0], tolerance): + match_i = i if i != len(self.vertices) - 1 else -1 + break + + # check equivalency of each vertex + if match_i is None: + return False + elif self[match_i - 1].is_equivalent(face[1], tolerance): + for i in xrange(len(self.vertices)): + if self[match_i - i].is_equivalent(face[i], tolerance) is False: + return False + elif self[match_i + 1].is_equivalent(face[1], tolerance): + for i in xrange(0, -len(self.vertices), -1): + if self[match_i + i].is_equivalent(face[i], tolerance) is False: + return False + else: + return False + return True
+ + +
+[docs] + def is_centered_adjacent(self, face, tolerance): + """Check whether a given face is centered adjacent with this Face. + + Centered adjacency is defined as sharing the same center point as this face + and being next to one another to within the tolerance. + + This is useful for identifying matching faces when you want to quickly + solve for adjacency and you are not concerned about false positives in cases + where one face does not perfectly match the other in terms of vertex ordering. + + Args: + face: Another face for which centered adjacency will be tested. + tolerance: The minimum difference between the coordinate values of two + centers at which they can be considered centered adjacent. + Returns: + True if centered adjacent. False if not centered adjacent. + """ + if not self.center.is_equivalent(face.center, tolerance): # center check + return False + # construct a ray using this face's normal and a point just behind this face + point_on_face = self._point_on_face(tolerance) + point_on_face = point_on_face - (self.normal * tolerance) # move below + test_ray = Ray3D(point_on_face, self.normal) + # shoot ray from this face to the other to verify adjacency + if face.intersect_line_ray(test_ray): + return True + return False
+ + +
+[docs] + def is_overlapping(self, face, tolerance): + """Check whether a this face overlaps with another given tolerance. + + Overlapping faces must not only be coplanar but they also have overlapping + polygons when evaluated within the same plane. Note that, if the face + is a sub-face of this one, this method will return True. + + Args: + face: A neighboring Face3D for which overlaps will be checked. + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. + + Returns: + True if the face is overlapping with this one. False if it is not. + """ + if not self.is_coplanar(face, tolerance): + return False + verts2d = tuple(self.plane.xyz_to_xy(_v) for _v in face.vertices) + other_poly = Polygon2D(verts2d) + if self.polygon2d.polygon_relationship(other_poly, tolerance) == -1: + return False + return True
+ + +
+[docs] + def is_sub_face(self, face, tolerance, angle_tolerance): + """Check whether a given face is a sub-face of this face. + + Sub-faces will lie in the same plane as this one and have all of their + vertices completely within the boundary of this face. + + This is useful for identifying whether a given sub-face (ie. a window or door) + can be assigned as a child to this face. + + Args: + face: Another face for which sub-face equivalency will be tested. + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. + angle_tolerance: The max angle in radians that the plane normals can + differ from one another in order for them to be considered coplanar. + + Returns: + True if it can be a valid sub-face. False if it is not a valid sub-face. + """ + # test whether the surface is coplanar + if not self.plane.is_coplanar_tolerance(face.plane, tolerance, angle_tolerance): + return False + + # if it is, convert sub-face to a polygon in this face's plane + return self._is_sub_face(face)
+ + +
+[docs] + def polygon_in_face(self, sub_face, origin=None, flip=False): + """Get a Polygon2D for a sub_face within the plane of this Face3D. + + Note that there is no check within this method to determine whether the + the sub_face is coplanar with this Face3D or is fully bounded by it. + So the is_sub_face method should be used to evaluate this before using + this method. + + Args: + sub_face: A Face3D for which a Polygon2D in the plane of this + Face3D will be returned. + origin: An optional Point3D to set the origin of the plane in which + the sub_face will be evaluated. Plugging in values like the + Face's lower_left_corner can standardize the geometry rules + for the resulting polygon. If None, this face's own + plane will be used. (Default: None). + flip: Boolean to note whether the x-axis of the plane should be flipped + when translating this the sub_face vertices. + """ + # set the process the origin into a plane + if origin is None: + plane = self.plane if not flip else self.plane.flip() + else: + if self._plane.n.z in (1, -1): + plane = Plane(self._plane.n, origin, Vector3D(1, 0, 0)) if not flip \ + else Plane(self._plane.n, origin, Vector3D(-1, 0, 0)) + else: + proj_y = Vector3D(0, 0, 1).project(self._plane.n) + proj_x = proj_y.rotate(self._plane.n, math.pi / -2) + plane = Plane(self._plane.n, origin, proj_x) + pts_2d = tuple(plane.xyz_to_xy(pt) for pt in sub_face.boundary) + return Polygon2D(pts_2d)
+ + +
+[docs] + def is_point_on_face(self, point, tolerance): + """Check whether a given point is on this face. + + This includes both a check to be sure that the point is in the plane of this + face and a check to ensure that point lies in the boundary of the face. + + Args: + point: A Point3D to evaluate whether it lies on the face. + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. + Returns: + True if the point is on the face. False if it is not. + """ + # test whether the point is in the plane of the face + if self.plane.distance_to_point(point) > tolerance: + return False + # if it is, convert the point into this face's plane + vert2d = self.plane.xyz_to_xy(point) + return self.polygon2d.is_point_inside(vert2d)
+ + +
+[docs] + def check_planar(self, tolerance, raise_exception=True): + """Check that all of the face's vertices lie within the face's plane. + + This check is not done by default when creating the face since + it is assumed that there is likely a check for planarity before the face + is created (ie. in CAD software where the face likely originates from). + This method is intended for quality control checking when the origin of + face geometry is unknown or is known to come from a place where no + planarity check was performed. + + Args: + tolerance: The minimum distance between a given vertex and a the + face's plane at which the vertex is said to lie in the plane. + raise_exception: Boolean to note whether an exception should be raised + if a vertex does not lie within the face's plane. If True, an + exception message will be given in such cases, which notes the non-planar + vertex and its distance from the plane. If False, this method will + simply return a False boolean if a vertex is found that is out + of plane. Default is True to raise an exception. + + Returns: + True if planar within the tolerance. False if not planar. + """ + for _v in self.vertices: + if self._plane.distance_to_point(_v) >= tolerance: + if raise_exception is True: + raise ValueError( + 'Vertex {} is out of plane with its parent face.\nDistance ' + 'to plane is {}'.format(_v, self._plane.distance_to_point(_v))) + else: + return False + return True
+ + +
+[docs] + def non_planar_vertices(self, tolerance): + """Get a tuple of Point3D for any vertices that lie outside the face's plane. + + This will be an empty tuple when the Face3D is planar and it is recommended + that the Face3D.check_planar method be used before calling this one. + + Args: + tolerance: The minimum distance between a given vertex and a the + face's plane at which the vertex is said to lie in the plane. + """ + np_verts = [] + for _v in self.vertices: + if self._plane.distance_to_point(_v) >= tolerance: + np_verts.append(_v) + return tuple(np_verts)
+ + +
+[docs] + def remove_duplicate_vertices(self, tolerance): + """Get a version of this face without duplicate vertices. + + Args: + tolerance: The minimum distance between a two vertices at which + they are considered co-located or duplicated. + """ + if not self.has_holes: # we only need to evaluate one list of vertices + new_vertices = tuple( + pt for i, pt in enumerate(self._vertices) + if not pt.is_equivalent(self._vertices[i - 1], tolerance)) + _new_face = Face3D(new_vertices, self.plane, enforce_right_hand=False) + return _new_face + # the face has holes + _boundary = tuple( + pt for i, pt in enumerate(self._boundary) + if not pt.is_equivalent(self._boundary[i - 1], tolerance)) + _holes = tuple( + tuple(p for i, p in enumerate(h) if not p.is_equivalent(h[i - 1], tolerance)) + for j, h in enumerate(self._holes)) + _new_face = Face3D(_boundary, self.plane, _holes, enforce_right_hand=False) + return _new_face
+ + +
+[docs] + def remove_colinear_vertices(self, tolerance): + """Get a version of this face without colinear or duplicate vertices. + + Args: + tolerance: The minimum distance between a vertex and the boundary segments + at which point the vertex is considered colinear. + """ + if not self.has_holes: # we only need to evaluate one list of vertices + new_vertices = self._remove_colinear( + self._vertices, self.polygon2d, tolerance) + _new_face = Face3D(new_vertices, self.plane, enforce_right_hand=False) + return _new_face + # the face has holes + _boundary = self._remove_colinear( + self._boundary, self.boundary_polygon2d, tolerance) + _holes = tuple(self._remove_colinear(hole, self.hole_polygon2d[i], tolerance) + for i, hole in enumerate(self._holes)) + _new_face = Face3D(_boundary, self.plane, _holes, enforce_right_hand=False) + return _new_face
+ + +
+[docs] + def flip(self): + """Get a face with a flipped direction from this one.""" + _new_face = Face3D(reversed(self.vertices), self.plane.flip(), + enforce_right_hand=False) + self._transfer_properties(_new_face) + if self._holes is not None: + _new_face._boundary = tuple(reversed(self._boundary)) + _new_face._holes = self._holes + return _new_face
+ + +
+[docs] + def move(self, moving_vec): + """Get a face that has been moved along a vector. + + Args: + moving_vec: A Vector3D with the direction and distance to move the face. + """ + _verts = self._move(self.vertices, moving_vec) + _new_face = self._face_transform(_verts, self.plane.move(moving_vec)) + if self._holes is not None: + _new_face._boundary = self._move(self._boundary, moving_vec) + _new_face._holes = tuple(self._move(hole, moving_vec) + for hole in self._holes) + return _new_face
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate a face by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + _verts = self._rotate(self.vertices, axis, angle, origin) + _new_face = self._face_transform(_verts, self.plane.rotate(axis, angle, origin)) + if self._holes is not None: + _new_face._boundary = self._rotate(self._boundary, axis, angle, origin) + _new_face._holes = tuple(self._rotate(hole, axis, angle, origin) + for hole in self._holes) + return _new_face
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a face rotated counterclockwise in the world XY plane by a certain angle. + + Args: + angle: An angle in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + _verts = self._rotate_xy(self.vertices, angle, origin) + _new_face = self._face_transform(_verts, self.plane.rotate_xy(angle, origin)) + if self._holes is not None: + _new_face._boundary = self._rotate_xy(self._boundary, angle, origin) + _new_face._holes = tuple(self._rotate_xy(hole, angle, origin) + for hole in self._holes) + return _new_face
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a face reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the face will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin from which to reflect. + """ + _verts = self._reflect(self.vertices, normal, origin) + _new_face = self._face_transform_reflect( + _verts, self.plane.reflect(normal, origin)) + if self._holes is not None: + _new_face._boundary = self._reflect(self._boundary, normal, origin) + _new_face._holes = tuple(self._reflect(hole, normal, origin) + for hole in self._holes) + return _new_face
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a face by a factor from an origin point. + + Args: + factor: A number representing how much the face should be scaled. + origin: A Point3D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + _verts = self._scale(self.vertices, factor, origin) + _new_face = self._face_transform_scale(_verts, None, factor) + if self._holes is not None: + _new_face._boundary = self._scale(self._boundary, factor, origin) + _new_face._holes = tuple(self._scale(hole, factor, origin) + for hole in self._holes) + return _new_face
+ + +
+[docs] + def split_through_holes(self): + """Get this Face3D split through its holes to get Face3D without holes. + + This method attempts to return the minimum number of non-holed shapes that + are needed to represent the original Face3D. If this fails, the result + will be derived from a triangulated shape. If getting a minimum number + of constituent Face3D is not important, it is more efficient to just + use all of the triangles in Face3D.triangulated_mesh3d instead of the + result of this method. + + Returns: + A list of Face3D without holes that together form a geometric + representation of this Face3D. If this Face3D has no holes a list + with a single Face3D is returned. + """ + def _shared_vertex_count(vert_set, verts): + """Get the number of shared vertices.""" + in_set = tuple(v for v in verts if v in vert_set) + return len(in_set) + + def _shared_edge_count(edge_set, verts): + """Get the number of shared edges.""" + edges = tuple((verts[i], verts[i - 1]) for i in range(3)) + in_set = tuple(e for e in edges if e in edge_set) + return len(in_set) + + if not self.has_holes: + return (self,) + # check that the direction of vertices for the hole is opposite the boundary + boundary = list(self.boundary_polygon2d.vertices) + holes = [list(hole.vertices) for hole in self.hole_polygon2d] + bound_direction = Polygon2D._are_clockwise(boundary) + for hole in holes: + if Polygon2D._are_clockwise(hole) is bound_direction: + hole.reverse() + # try to split the polygon neatly in two + s_result = Polygon2D._merge_boundary_and_holes(boundary, holes, split=True) + if s_result is not None: + poly_1, poly_2 = s_result + vert_1 = tuple(self.plane.xy_to_xyz(pt) for pt in poly_1) + vert_2 = tuple(self.plane.xy_to_xyz(pt) for pt in poly_2) + face_1 = Face3D(vert_1, plane=self.plane) + face_2 = Face3D(vert_2, plane=self.plane) + return face_1, face_2 + # if splitting in two did not work, then triangulate it and merge them together + tri_mesh = self.triangulated_mesh3d + tri_verts = tri_mesh.vertices + rel_f = tri_mesh.faces[0] + tri_faces = [[tuple(tri_verts[pt] for pt in rel_f)]] + tri_face_sets = [set(rel_f)] + tri_edge_sets = [set((rel_f[i - 1], rel_f[i]) for i in range(3))] + faces_to_test = list(tri_mesh.faces[1:]) + # group the faces along matched edges + for f in faces_to_test: + connected = False + for tfs, fs, es in zip(tri_faces, tri_face_sets, tri_edge_sets): + svc = _shared_vertex_count(fs, f) + sec = _shared_edge_count(es, f) + if svc == 2 and sec == 1: # matched edge + tfs.append(tuple(tri_verts[pt] for pt in f)) + for i, v in enumerate(f): + fs.add(v) + es.add((f[i - 1], f[i])) + break + elif svc == 3: # definitely a new shape + connected = True + else: # not ready to be merged; put it to the back + if connected: + tri_faces.append([tuple(tri_verts[pt] for pt in f)]) + tri_face_sets.append(set(f)) + tri_edge_sets.append(set((f[i - 1], f[i]) for i in range(3))) + else: + faces_to_test.append(f) + # create Face3Ds from the triangle groups + final_faces = [] + for tf in tri_faces: + t_mesh = Mesh3D.from_face_vertices(tf) + ed_len = (seg.length for seg in t_mesh.naked_edges) + tol = min(ed_len) / 10 + f_bound = Polyline3D.join_segments(t_mesh.naked_edges, tol) + final_faces.append(Face3D(f_bound[0].vertices, plane=self.plane)) + return final_faces
+ + +
+[docs] + def split_with_line(self, line, tolerance): + """Split this face into two or more Face3D given a LineSegment3D. + + If the input line is found to not exist in the plane of this Face3D + or it does not intersect this Face3D in a manner that splits it into two + or more pieces, None will be returned. + + Args: + line: A LineSegment3D object in the plane of this Face3D, which will + be used to split it into two or more pieces. + tolerance: The maximum difference between point values for them to be + considered distinct from one another. + + Returns: + A list of Face3D for the result of splitting this Face3D with the + input line. Will be None if the line is not in the plane of the + Face3D or if it does not split the Face3D into two or more pieces. + """ + # first check that the line is in the plane of the Face3D + if self.plane.distance_to_point(line.p1) > tolerance or \ + self.plane.distance_to_point(line.p1) > tolerance: + return None + + # change the line and face to be in 2D and check that it can split the Face + prim_pl = self.plane + bnd_poly = self.boundary_polygon2d + hole_polys = self.hole_polygon2d + line_2d = LineSegment2D.from_end_points( + prim_pl.xyz_to_xy(line.p1), prim_pl.xyz_to_xy(line.p2)) + if not Polygon2D.overlapping_bounding_rect(bnd_poly, line_2d, tolerance): + return None + + # create the network object and use it to find the cycles + dg = DirectedGraphNetwork.from_shape_to_split( + bnd_poly, hole_polys, [line_2d], tolerance) + split_faces = [] + for cycle in dg.all_min_cycles(): + if len(cycle) >= 3: + pt_3ds = [prim_pl.xy_to_xyz(node.pt) for node in cycle] + new_face = Face3D(pt_3ds, plane=prim_pl) + try: + new_face = new_face.remove_colinear_vertices(tolerance) + split_faces.append(new_face) + except AssertionError: # degenerate geometry to ignore + pass + + # rebuild the Face3D from the results and return them + if len(split_faces) == 1: + return split_faces + return Face3D.merge_faces_to_holes(split_faces, tolerance)
+ + +
+[docs] + def split_with_polyline(self, polyline, tolerance): + """Split this face into two or more Face3D given an open Polyline3D. + + If the input polyline is found to not exist in the plane of this Face3D, + or the polyline is self-intersecting (or closed), or it does not intersect + this Face3D in a manner that splits it into two or more pieces, None + will be returned. + + Note that, if you wish to use an operation similar to this method but + with a closed Polyline3D (effectively another Face3D), then the + Face3D.coplanar_split() method should be used instead of this method. + + Args: + polyline: A Polyline3D object in the plane of this Face3D, which will + be used to split it into two or more pieces. + tolerance: The maximum difference between point values for them to be + considered distinct from one another. + + Returns: + A list of Face3D for the result of splitting this Face3D with the + input polyline. Will be None if the polyline is not in the plane of + the Face3D, or the polyline intersects itself (or is closed), or if it + does not split the Face3D into two or more pieces. + """ + # first check that the polyline is in the plane of the Face3D + for pl_pt in polyline.vertices: + if self.plane.distance_to_point(pl_pt) > tolerance: + return None + + # change the polyline and face to be in 2D and check that it can split the Face + prim_pl = self.plane + bnd_poly = self.boundary_polygon2d + hole_polys = self.hole_polygon2d + polyline_2d = Polyline2D([prim_pl.xyz_to_xy(pt) for pt in polyline]) + if not Polygon2D.overlapping_bounding_rect(bnd_poly, polyline_2d, tolerance): + return None + rel_line_2ds = [] + intersect_count = 0 + for seg in polyline_2d.segments: + intersect_count += len(bnd_poly.intersect_line_ray(seg)) + if seg.length > tolerance and \ + Polygon2D.overlapping_bounding_rect(bnd_poly, seg, tolerance): + rel_line_2ds.append(seg) + if len(rel_line_2ds) == 0: + return None + + # create the network object and use it to find the cycles + dg = DirectedGraphNetwork.from_shape_to_split( + bnd_poly, hole_polys, polyline_2d.segments, tolerance) + split_faces = [] + for cycle in dg.all_min_cycles(): + if len(cycle) >= 3: + pt_3ds = [prim_pl.xy_to_xyz(node.pt) for node in cycle] + new_face = Face3D(pt_3ds, plane=prim_pl) + try: + new_face = new_face.remove_colinear_vertices(tolerance) + split_faces.append(new_face) + except AssertionError: # degenerate geometry to ignore + pass + + # rebuild the Face3D from the results and return them + if len(split_faces) == 1: + return split_faces + return Face3D.merge_faces_to_holes(split_faces, tolerance)
+ + +
+[docs] + def split_with_lines(self, lines, tolerance): + """Split this face into two or more Face3D given multiple LineSegment3D. + + Using this method is distinct from looping over Face3D.split_with_line + in that this method will resolve cases where multiple segments branch out + from nodes in a network of input lines. So, if three line segments + meet at a point in the middle of this Face3D and each extend past the + edges of this Face3D, this method can split the Face3D in 3 parts whereas + looping over the Face3D.split_with_line will not do this given that each + individual segment cannot split the Face3D. + + If the input lines together do not intersect this Face3D in a manner + that splits it into two or more pieces, None will be returned. + + Args: + lines: A list of LineSegment3D objects in the plane of this Face3D, + which will be used to split it into two or more pieces. + tolerance: The maximum difference between point values for them to be + considered distinct from one another. + + Returns: + A list of Face3D for the result of splitting this Face3D with the + input lines. Will be None if the line is not in the plane of the + Face3D or if it does not split the Face3D into two or more pieces. + """ + # first check that the lines are in the plane of the Face3D + rel_line_3ds = [] + for line in lines: + if self.plane.distance_to_point(line.p1) <= tolerance or \ + self.plane.distance_to_point(line.p1) <= tolerance: + rel_line_3ds.append(line) + if len(rel_line_3ds) == 0: + return None + + # change the line and face to be in 2D and check that it can split the Face + prim_pl = self.plane + bnd_poly = self.boundary_polygon2d + hole_polys = self.hole_polygon2d + rel_line_2ds = [] + for line in rel_line_3ds: + line_2d = LineSegment2D.from_end_points( + prim_pl.xyz_to_xy(line.p1), prim_pl.xyz_to_xy(line.p2)) + if line_2d.length > tolerance and \ + Polygon2D.overlapping_bounding_rect(bnd_poly, line_2d, tolerance): + rel_line_2ds.append(line_2d) + if len(rel_line_2ds) == 0: + return None + + # create the network object and use it to find the cycles + dg = DirectedGraphNetwork.from_shape_to_split( + bnd_poly, hole_polys, rel_line_2ds, tolerance) + split_faces = [] + for cycle in dg.all_min_cycles(): + if len(cycle) >= 3: + pt_3ds = [prim_pl.xy_to_xyz(node.pt) for node in cycle] + new_face = Face3D(pt_3ds, plane=prim_pl) + try: + new_face = new_face.remove_colinear_vertices(tolerance) + split_faces.append(new_face) + except AssertionError: # degenerate geometry to ignore + pass + + # rebuild the Face3D from the results and return them + if len(split_faces) == 1: + return split_faces + return Face3D.merge_faces_to_holes(split_faces, tolerance)
+ + +
+[docs] + def intersect_line_ray(self, line_ray): + """Get the intersection between this face and the input LineSegment3D or Ray3D. + + Args: + line_ray: A LineSegment3D or Ray3D object for which intersection + will be computed. + + Returns: + Point3D for the intersection. Will be None if no intersection exists. + """ + _plane_int = self._plane.intersect_line_ray(line_ray) + if _plane_int is not None: + _int2d = self._plane.xyz_to_xy(_plane_int) + if self.polygon2d.is_point_inside_bound_rect(_int2d): + return _plane_int + return None
+ + +
+[docs] + def intersect_plane(self, plane): + """Get the intersection between this face and the input plane. + + Args: + plane: A Plane object for which intersection will be computed. + + Returns: + List of LineSegment3D objects for the intersection. + Will be None if no intersection exists. + """ + _plane_int_ray = self._plane.intersect_plane(plane) + if _plane_int_ray is not None: + _p12d = self._plane.xyz_to_xy(_plane_int_ray.p) + _p22d = self._plane.xyz_to_xy(_plane_int_ray.p + _plane_int_ray.v) + _v2d = _p22d - _p12d + _int_ray2d = Ray2D(_p12d, _v2d) + _int_pt2d = self.polygon2d.intersect_line_infinite(_int_ray2d) + if len(_int_pt2d) != 0: + if len(_int_pt2d) > 2: # sort the points along the intersection line + _int_pt2d.sort(key=lambda pt: pt.x) + _int_pt3d = [self._plane.xy_to_xyz(pt) for pt in _int_pt2d] + _int_seg3d = [] + for i in xrange(0, len(_int_pt3d) - 1, 2): + _int_seg3d.append(LineSegment3D.from_end_points( + _int_pt3d[i], _int_pt3d[i + 1])) + return _int_seg3d + return None
+ + +
+[docs] + def project_point(self, point): + """Project a Point3D onto this face. + + Note that this method does a check to see if the point can be projected to + within this face's boundary. If all that is needed is a point projected + into the plane of this face, the Plane.project_point() method should be + used with this face's plane property. + + Args: + point: A Point3D object to project. + + Returns: + Point3D for the point projected onto this face. Will be None if the + point cannot be projected to within the boundary of the face. + """ + _plane_int = point.project(self._plane.n, self._plane.o) + _plane_int2d = self._plane.xyz_to_xy(_plane_int) + if self.polygon2d.is_point_inside_bound_rect(_plane_int2d): + return _plane_int + return None
+ + +
+[docs] + def mesh_grid(self, x_dim, y_dim=None, offset=None, flip=False, + generate_centroids=True): + """Get a gridded Mesh3D over this face. + + This method generates a mesh grid over the domain of the face + and then removes any vertices that do not lie within it. + + Note that the x_dim and y_dim refer to dimensions within the X and Y + coordinate system of this faces's plane. So rotating this plane will + result in rotated grid cells. + + Args: + x_dim: The x dimension of the grid cells as a number. + y_dim: The y dimension of the grid cells as a number. Default is None, + which will assume the same cell dimension for y as is set for x. + offset: A number for how far to offset the grid from the base face. + Default is None, which will not offset the grid at all. + flip: Set to True to have the mesh normals reversed from the direction + of this face and to have the offset input move the mesh in the + opposite direction from this face's normal. + generate_centroids: Set to True to have the face centroids generated + alongside the grid of vertices, which is much faster than having + them generated upon request as they typically are. However, if you + have no need for the face centroids, you would save time and memory + by setting this to False. Default is True. + """ + # check the inputs and set defaults + self._check_number_mesh_grid(x_dim, 'x_dim') + if y_dim is not None: + self._check_number_mesh_grid(y_dim, 'y_dim') + else: + y_dim = x_dim + if offset is not None: + self._check_number_mesh_grid(offset, 'offset') + + # generate the mesh grid and convert it to a 3D mesh + grid_mesh2d = Mesh2D.from_polygon_grid( + self.polygon2d, x_dim, y_dim, generate_centroids) + if offset is None or offset == 0: + vert_3d = tuple(self._plane.xy_to_xyz(pt) + for pt in grid_mesh2d.vertices) + else: + _off_num = -1 * offset if flip is True else offset + _off_plane = self.plane.move(self.plane.n * _off_num) + vert_3d = tuple(_off_plane.xy_to_xyz(pt) + for pt in grid_mesh2d.vertices) + grid_mesh3d = Mesh3D(vert_3d, grid_mesh2d.faces) + grid_mesh3d._face_areas = grid_mesh2d._face_areas + + # assign the face plane normal to the mesh normals + if flip is True: + grid_mesh3d._face_normals = self._plane.n.reverse() + grid_mesh3d._vertex_normals = self._plane.n.reverse() + grid_mesh3d._faces = tuple( + tuple(reversed(face)) for face in grid_mesh3d._faces) # right-hand rule + else: + grid_mesh3d._face_normals = self._plane.n + grid_mesh3d._vertex_normals = self._plane.n + + # transform the centroids to 3D space if they were generated + if generate_centroids is True: + _conv_plane = self._plane if offset is None or offset == 0 else _off_plane + grid_mesh3d._face_centroids = tuple(_conv_plane.xy_to_xyz(pt) + for pt in grid_mesh2d.face_centroids) + + return grid_mesh3d
+ + +
+[docs] + def contour_by_number(self, contour_count, direction_vector, flip_side, tolerance): + """Generate a list of LineSegment3D objects contouring the face. + + Args: + contour_count: A positive integer for the number of contours + to generate over the face. + direction_vector: A Vector2D for the direction along which contours + are generated. This 2D vector will be interpreted into a 3D vector + within the plane of this Face. (0, 1) will usually generate + horizontal contours in 3D space, (1, 0) will generate vertical + contours, and (1, 1) will generate diagonal contours. Recommended + value is Vector2D(0, 1). + flip_side: Boolean to note whether the side the contours start from + should be flipped. Recommended value is False to have contours + on top or right. + Setting to True will start contours on the bottom or left. + tolerance: The minimum distance between coordinates that is considered + meaningful. Will be used to remove any contours with a length less + than the tolerance. + """ + # interpret the 2D direction_vector into one that exists in 3D space + ref_plane = Plane(self._plane.n, Point3D(0, 0, 0), self._plane.x) + if ref_plane.y.z < 0: + ref_plane = ref_plane.rotate(ref_plane.n, math.pi, ref_plane.o) + plane_normal = ref_plane.xy_to_xyz(direction_vector).normalize() + + # get a diagonal going across the face + diagonal = self._diagonal_along_self(direction_vector, tolerance) + if not flip_side: + diagonal = diagonal.flip() # flip diagonal if user has requested it + + # generate the contours + contours = [] + for pt in diagonal.subdivide_evenly(contour_count)[:-1]: + result = self.intersect_plane(Plane(plane_normal, pt)) + if result is not None: + contours.extend(result) + + # remove any contours that are smaller than the tolerance. + if tolerance != 0: + contours = [l_seg for l_seg in contours if l_seg.length >= tolerance] + return contours
+ + +
+[docs] + def contour_by_distance_between(self, distance, direction_vector, flip_side, + tolerance): + """Generate a list of LineSegment3D objects contouring the face. + + Args: + distance: A number for the distance between each contour. + direction_vector: A Vector2D for the direction along which contours + are generated. This 2D vector will be interpreted into a 3D vector + within the plane of this Face. (0, 1) will usually generate + horizontal contours in 3D space, (1, 0) will generate vertical + contours, and (1, 1) will generate diagonal contours. Recommended + value is Vector2D(0, 1). + flip_side: Boolean to note whether the side the contours start from + should be flipped. Recommended value is is False to have contours + start on top or right. Setting to True will start contours on + the bottom or left. + tolerance: The minimum distance between coordinates that is considered + meaningful. Will be used to remove any contours with a length less + than the tolerance. + """ + # interpret the 2D direction_vector into one that exists in 3D space + ref_plane = Plane(self._plane.n, Point3D(0, 0, 0), self._plane.x) + if ref_plane.y.z < 0: + ref_plane = ref_plane.rotate(ref_plane.n, math.pi, ref_plane.o) + plane_normal = ref_plane.xy_to_xyz(direction_vector).normalize() + + # get a diagonal going across the face + diagonal = self._diagonal_along_self(direction_vector, tolerance) + if not flip_side: + diagonal = diagonal.flip() # flip diagonal if user has requested it + + # compute the diagonal subdivision distance using the plane_normal + angle = plane_normal.angle(diagonal.v) + angle = abs(angle - math.pi) if angle > math.pi / 2 else angle + proj_dist = distance / math.cos(angle) + + # generate the contours + contours = [] + for pt in diagonal.subdivide(proj_dist)[:-1]: + pass + result = self.intersect_plane(Plane(plane_normal, pt)) + if result is not None: + contours.extend(result) + + # remove any contours that are smaller than the tolerance. + if tolerance != 0: + contours = [l_seg for l_seg in contours if l_seg.length >= tolerance] + return contours
+ + +
+[docs] + def contour_fins_by_number(self, fin_count, depth, offset, angle, + contour_vector, flip_side, tolerance): + """Generate a list of Fac3D objects over this face (like louvers or fins). + + Args: + fin_count: A positive integer for the number of fins to generate. + depth: A number for the depth to extrude the fins. + offset: A number for the distance to offset fins from this face. + Recommended value is 0 for no offset. + angle: A number for the for an angle to rotate the fins in radians. + Recommended value is 0 for no rotation. + contour_vector: A Vector2D for the direction along which contours + are generated. This 2D vector will be interpreted into a 3D vector + within the plane of this Face. (0, 1) will usually generate + horizontal contours in 3D space, (1, 0) will generate vertical + contours, and (1, 1) will generate diagonal contours. Recommended + value is Vector2D(0, 1). + flip_side: Boolean to note whether the side the fins start from + should be flipped. Recommended value is False to have contours + start on top or right. Setting to True will start contours on + the bottom or left. + tolerance: The minimum distance between coordinates that is considered + meaningful. Will be used to remove any contours with a length less + than the tolerance. + """ + extru_vec = self._get_fin_extrusion_vector(depth, angle, contour_vector) + contours = self.contour_by_number( + fin_count, contour_vector, flip_side, tolerance) + return self._get_extrusion_fins(contours, extru_vec, offset)
+ + +
+[docs] + def contour_fins_by_distance_between(self, distance, depth, offset, angle, + contour_vector, flip_side, tolerance): + """Generate a list of Fac3D objects over this face (like louvers or fins). + + Args: + distance: A number for the approximate distance between each contour. + depth: A number for the depth to extrude the fins. + offset: A number for the distance to offset fins from this face. + Recommended value is 0 for no offset. + angle: A number for the for an angle to rotate the fins in radians. + Recommended value is 0 for no rotation. + contour_vector: A Vector2D for the direction along which contours + are generated. This 2D vector will be interpreted into a 3D vector + within the plane of this Face. (0, 1) will usually generate + horizontal contours in 3D space, (1, 0) will generate vertical + contours, and (1, 1) will generate diagonal contours. Recommended + value is Vector2D(0, 1). + flip_side: Boolean to note whether the side the fins start from + should be flipped. Recommended value is False to have contours + start on top or right. Setting to True will start contours on + the bottom or left. + tolerance: The minimum distance between coordinates that is considered + meaningful. Will be used to remove any contours with a length less + than the tolerance. + """ + extru_vec = self._get_fin_extrusion_vector(depth, angle, contour_vector) + contours = self.contour_by_distance_between( + distance, contour_vector, flip_side, tolerance) + return self._get_extrusion_fins(contours, extru_vec, offset)
+ + +
+[docs] + def sub_faces_by_ratio(self, ratio): + """Get a list of faces with a combined area equal to ratio times this face area. + + All sub faces will lie inside the boundaries of this face and will have + the same normal as this face. + + Args: + ratio: A number between 0 and 1 for the ratio between the area of + the sub faces and the area of this face. + + Returns: + A list of Face3D objects for sub faces. + """ + scale_factor = ratio ** .5 + if self.is_convex: + return [self.scale(scale_factor, self.centroid)] + else: + _tri_mesh = self.triangulated_mesh3d + _tri_faces = [[_tri_mesh[i] for i in face] for face in _tri_mesh.faces] + _scaled_verts = [] + for i, _tri in enumerate(_tri_faces): + _scaled_verts.append( + [pt.scale(scale_factor, _tri_mesh.face_centroids[i]) for pt in _tri]) + return [Face3D(_t, self.plane) for _t in _scaled_verts]
+ + +
+[docs] + def sub_faces_by_ratio_gridded(self, ratio, x_dim, y_dim=None): + """Get a list of faces with a combined area equal to ratio times this face area. + + All sub faces will lie inside the boundaries of this face and have the same + normal as this face. + + Sub faces will be arranged in a grid derived from this face's plane property. + Because the x_dim and y_dim refer to dimensions within the X and Y + coordinate system of this faces's plane, rotating this plane will + result in rotated grid cells. + + If the x_dim and/or y_dim are too large for this face, this method will + return essentially the same result as the sub_faces_by_ratio method. + + Args: + ratio: A number between 0 and 1 for the ratio between the area of + the sub faces and the area of this face. + x_dim: The x dimension of the grid cells as a number. + y_dim: The y dimension of the grid cells as a number. Default is None, + which will assume the same cell dimension for y as is set for x. + + Returns: + A list of Face3D objects for sub faces. + """ + try: # get the gridded mesh derived from this face + grid_mesh = self.mesh_grid(x_dim, y_dim) + except AssertionError: # there are no faces; just return sub_faces_by_ratio + return self.sub_faces_by_ratio(ratio) + + # compute the area that each of the mesh faces need to be scaled to + _verts, _faces = grid_mesh.vertices, grid_mesh.faces + _x_dim = _verts[_faces[0][0]].distance_to_point(_verts[_faces[0][1]]) + _y_dim = _verts[_faces[0][1]].distance_to_point(_verts[_faces[0][2]]) + fac = (self.area * ratio) / (_x_dim * _y_dim * len(_faces)) + + # if the factor is greater than 1, sub-faces will be overlapping + if fac >= 1: + return self.sub_faces_by_ratio(ratio) + s_fac = fac ** 0.5 + + # generate the Face3D objects while scaling them to the correct size + sub_faces = [] + for face, centr in zip(_faces, grid_mesh.face_centroids): + _f = Face3D(tuple(_verts[i].scale(s_fac, centr) for i in face), self.plane) + if self._is_sub_face(_f): # catch edge cases + sub_faces.append(_f) + return sub_faces
+ + +
+[docs] + def sub_faces_by_ratio_rectangle(self, ratio, tolerance): + """Get a list of faces with a combined area equal to ratio times this face area. + + This function is virtually equivalent to the sub_faces_by_ratio method + but a check will be performed to see if any rectangles can be pulled out + of this face's geometry. This tends to make the result a bit cleaner, + especially for concave faces that have rectangles (like L-shaped faces). + + Args: + ratio: A number between 0 and 1 for the ratio between the area of + the sub faces and the area of this face. + tolerance: The maximum difference between point values for them to be + considered a part of a rectangle. + + Returns: + A list of Face3D objects for sub faces. If there is a rectangle in this + shape, the scaled rectangle will be the first item in this list. + """ + rect_res = self.extract_rectangle(tolerance) + if rect_res is None: + return self.sub_faces_by_ratio(ratio) + bottom_seg, top_seg, other_faces = rect_res + rect_face = Face3D((bottom_seg.p1, bottom_seg.p2, top_seg.p2, top_seg.p1), + self.plane) + scale_factor = ratio ** .5 + sub_faces = [rect_face.scale(scale_factor, rect_face.center)] + for face in other_faces: + sfs = face.sub_faces_by_ratio(ratio) + for sf in sfs: + if sf.area > tolerance: + sub_faces.append(sf) + return sub_faces
+ + +
+[docs] + def sub_faces_by_ratio_sub_rectangle(self, ratio, sub_rect_height, sill_height, + horizontal_separation, vertical_separation, + tolerance): + """Get a list of faces with a combined area equal to ratio times this face area. + + This function is virtually equivalent to the sub_faces_by_ratio_rectangle + method but any rectangles that are found will be broken down into sub-rectangles + using the other inputs (sub_rect_height, sill_height, horizontal_separation, + vertical_separation). This allows for the creation of a wide array of + rectangular sub-face geometries. + + Args: + ratio: A number between 0 and 1 for the ratio between the area of + the sub faces and the area of this face. + sub_rect_height: A number for the target height of the output sub- + rectangles. Note that, if the ratio is too large for the height, + the ratio will take precedence and the sub-rectangle height will + be larger than this value. + sill_height: A number for the target height above the bottom edge of + the rectangle to start the sub-rectangles. Note that, if the + ratio is too large for the height, the ratio will take precedence + and the sub-rectangle height will be smaller than this value. + horizontal_separation: A number for the target separation between + individual sub-rectangle center lines. If this number is larger than + the parent rectangle base, only one sub-rectangle will be produced. + vertical_separation: An optional number to create a single vertical + separation between top and bottom sub-rectangles. The default is + 0 for no separation. + tolerance: The maximum difference between point values for them to be + considered a part of a rectangle. + + Returns: + A list of Face3D objects for sub faces. If there is a rectangle in this + shape, the scaled rectangle will be the first item in this list. + """ + rect_res = self.extract_rectangle(tolerance) + if rect_res is None: + return self.sub_faces_by_ratio(ratio) + bottom_seg, top_seg, other_faces = rect_res + height_seg = LineSegment3D.from_end_points(bottom_seg.p, top_seg.p) + norm_tup = self._normal_from_3pts(bottom_seg.p, bottom_seg.p2, top_seg.p) + norm = Vector3D(*norm_tup).normalize() + base_plane = Plane(norm, bottom_seg.p, bottom_seg.v) + sub_faces = Face3D.sub_rects_from_rect_ratio( + base_plane, bottom_seg.length, height_seg.length, ratio, + sub_rect_height, sill_height, horizontal_separation, vertical_separation) + for face in other_faces: + sfs = face.sub_faces_by_ratio(ratio) + for sf in sfs: + if sf.area > tolerance: + sub_faces.append(sf) + return sub_faces
+ + +
+[docs] + def sub_faces_by_dimension_rectangle(self, sub_rect_height, sub_rect_width, + sill_height, horizontal_separation, tolerance): + """Get a list of rectangular faces within this Face3D. + + Note that this method will only yield results if there is a rectangle to + be extracted from this Face3D's geometry. + + Args: + sub_rect_height: A number for the target height of the output rectangles. + sub_rect_width: A number for the target width of the output rectangles. + sill_height: A number for the target height above the bottom edge of + the rectangle to start the sub-rectangles. If the sub_rect_height + is too large for the sill_height to fit within the rectangle, + the sub_rect_height will take precedence. + horizontal_separation: A number for the target separation between + individual sub-rectangle center lines. If this number is larger than + the parent rectangle base, only one sub-rectangle will be produced. + tolerance: The maximum difference between point values for them to be + considered a part of a rectangle. + + Returns: + A list of Face3D objects for sub faces. + """ + rect_res = self.extract_rectangle(tolerance) + if rect_res is None: + return [] + bottom_seg, top_seg, _ = rect_res + height_seg = LineSegment3D.from_end_points(bottom_seg.p, top_seg.p) + norm_tup = self._normal_from_3pts(bottom_seg.p, bottom_seg.p2, top_seg.p) + norm = Vector3D(*norm_tup).normalize() + base_plane = Plane(norm, bottom_seg.p, bottom_seg.v) + sub_faces = Face3D.sub_rects_from_rect_dimensions( + base_plane, bottom_seg.length, height_seg.length, sub_rect_height, + sub_rect_width, sill_height, horizontal_separation) + return sub_faces
+ + +
+[docs] + def get_top_bottom_horizontal_edges(self, tolerance): + """Get top and bottom horizontal edges of this Face if they exist. + + Args: + tolerance: The maximum difference between the z values of the start and + end coordinates at which an edge is considered horizontal. + + Returns: + (bottom_edge, top_edge) with each as LineSegment3D if they exist. + None if they do not exist. + """ + # test if each of the edges are vertical. + horizontal_edges = [] + for edge in self.boundary_segments: + if edge.is_horizontal(tolerance): + horizontal_edges.append(edge) + + if len(horizontal_edges) < 2: + return None + else: + sorted_edges = sorted(horizontal_edges, key=lambda edge: edge.p.z) + return sorted_edges[0], sorted_edges[1]
+ + +
+[docs] + def get_left_right_vertical_edges(self, tolerance): + """Get left and right vertical edges of this Face if they exist. + + Args: + tolerance: The maximum difference between the x any y values of the start + and end coordinates at which an edge is considered vertical. + + Returns: + (left_edge, right_edge) with each as LineSegment3D if they exist. Left in + this case is defined as the edge with the lower X coordinates. + Result will be None if vertical edges do not exist. + """ + # test if each of the edges are vertical. + vertical_edges = [] + for edge in self.boundary_segments: + if edge.is_vertical(tolerance): + vertical_edges.append(edge) + + if len(vertical_edges) < 2: + return None + else: + if abs(self.normal.x) != 1: + sorted_edges = sorted(vertical_edges, key=lambda edge: edge.p.x) + else: + sorted_edges = sorted(vertical_edges, key=lambda edge: edge.p.y) + return sorted_edges[0], sorted_edges[-1]
+ + +
+[docs] + def extract_rectangle(self, tolerance): + """Extract top and bottom line segments of a rectangle within this Face. + + This method will only return geometry if: + + 1) There are no holes in the face. + + 2) The face is not parallel to the World XY plane. + + 3) There are two parallel edges to this face, which are either + oriented horizontally or vertically. + + 4) There must be enough overlap between these edges for a rectangle + to be drawn between them. + + If this Face does not satisfy this criteria, None will be returned. + + Args: + tolerance: The maximum difference between point values for them to be + considered a part of a rectangle. + + Returns: + A tuple with three elements + + - bottom_edge: A LineSegment3D representing the bottom of the rectangle. + + - top_edge: A LineSegment3D representing the top of the rectangle. + + - other_faces: + A list of Face3D objects for the parts of this face not + included in the rectangle. The length of this list will be between + 0 (if this face is already rectangular) and 2 (if there are non- + rectangular geometries on either side of the rectangle.) + """ + # perform checks on the face to see if a rectangle is extractable + if self.has_holes: + return None + if abs(self.normal.x) <= tolerance and abs(self.normal.y) <= tolerance: + # face lies within a horizontal plane; we cannot distinguish top and bottom + return None + clean_face = self.remove_colinear_vertices(tolerance) + + # try to extract a rectangle from horizontal curves + horiz_result = clean_face.get_top_bottom_horizontal_edges(tolerance) + if horiz_result is not None: + bottom_seg, top_seg = horiz_result + split_res = clean_face._split_with_rectangle(bottom_seg, top_seg, tolerance) + if split_res is not None: + return LineSegment3D.from_end_points(split_res[0][1], split_res[0][3]), \ + LineSegment3D.from_end_points(split_res[0][0], split_res[0][2]), \ + split_res[1] + + # try to extract a rectangle from vertical curves + vert_result = clean_face.get_left_right_vertical_edges(tolerance) + if vert_result is not None: + left_seg, right_seg = vert_result + split_res = clean_face._split_with_rectangle(left_seg, right_seg, tolerance) + if split_res is not None: + seg_1 = LineSegment3D.from_end_points(split_res[0][0], split_res[0][1]) + seg_2 = LineSegment3D.from_end_points(split_res[0][2], split_res[0][3]) + sorted_edges = sorted([seg_1, seg_2], key=lambda edge: edge.p.z) + return sorted_edges[0], sorted_edges[1], split_res[1] + return None
+ + +
+[docs] + @staticmethod + def sub_rects_from_rect_ratio( + base_plane, parent_base, parent_height, ratio, sub_rect_height, sill_height, + horizontal_separation, vertical_separation=0): + """Get a list of rectangular Face3D objects using an area ratio and parameters. + + All of the resulting Face3D objects lie within a parent rectangle defined + by the parent_base, parent_height, and base_plane. The combined area of the + resulting rectangles is equal to the area of the larger rectangle multiplied + by the input ratio. This method is particularly useful for generating + rectangular window surfaces. + + Args: + base_plane: A Plane object in which the rectangle exists. + The origin of this plane will be the lower left corner of the + rectangle and the X and Y axes will form the sides. + parent_base: A number indicating the length of the base of the + parent rectangle. + parent_height: A number indicating the length of the height of the + parent rectangle. + ratio: A number between 0 and 1 for the ratio between the area of + the sub rectangle faces and the area of this face. + sub_rect_height: A number for the target height of the output sub- + rectangles. Note that, if the ratio is too large for the height, + the ratio will take precedence and the sub-rectangle height will + be larger than this value. + sill_height: A number for the target height above the bottom edge of + the rectangle to start the sub-rectangles. Note that, if the + ratio is too large for the height, the ratio will take precedence + and the sub-rectangle height will be smaller than this value. + horizontal_separation: A number for the target separation between + individual sub-rectangle center lines. If this number is larger than + the parent rectangle base, only one sub-rectangle will be produced. + vertical_separation: An optional number to create a single vertical + separation between top and bottom sub-rectangles. The default is + 0 for no separation. + + Returns: + A list of Face3D objects for sub faces. + """ + # calculate the target area to make the combined sub-rectangles + target_area = parent_base * parent_height * ratio + # find the maximum area for subdivision into smaller, taller sub-rectangles + max_area_subdiv = parent_base * 0.98 * sub_rect_height + # if sub_rect_height > parent_height, set it to just under parent_height + max_subh = 0.98 * parent_height + sub_rect_height = max_subh if sub_rect_height > max_subh else sub_rect_height + # if sill_height is close to 0, set it to just above 0. + min_sill = 0.01 * parent_height + sill_height = min_sill if sill_height < min_sill else sill_height + # properties used throughout the computation of sub-rectangles + bottom_seg = LineSegment3D.from_sdl(base_plane.o, base_plane.x, parent_base) + + if target_area < max_area_subdiv: + # divide up the rectangle into points on the bottom. + if parent_base > (horizontal_separation / 2): + num_div = round(parent_base / horizontal_separation, 0) + else: + num_div = 1 + btm_div_pts = bottom_seg.subdivide_evenly(num_div) + btm_div_segs = tuple(LineSegment3D.from_end_points(pt, btm_div_pts[i + 1]) + for i, pt in enumerate(btm_div_pts[:-1])) + # move the segments to the sill height + max_sill_h = parent_height * 0.99 - sub_rect_height + sill_vec = base_plane.y * sill_height if sill_height < max_sill_h \ + else base_plane.y * max_sill_h + div_segs = tuple(seg.move(sill_vec) for seg in btm_div_segs) + # scale the segments along their center points + seg_width = div_segs[0].length + subrect_width = (target_area / sub_rect_height) / num_div + scale_fac = subrect_width / seg_width + scaled_segs = [seg.scale(scale_fac, seg.midpoint) for seg in div_segs] + # find the maximum acceptable area for splitting the glazing vertically. + if vertical_separation != 0: + max_split_vert = parent_height - sill_height - sub_rect_height \ + - (0.02 * parent_height) + if vertical_separation < 0 or max_split_vert < 0: + vertical_separation = 0 + elif vertical_separation > max_split_vert: + vertical_separation = max_split_vert + # generate the vertices by 'extruding' along a window height vector. + final_faces = [] + if vertical_separation != 0: + sub_rect_height = sub_rect_height / 2 + h_vec = base_plane.y * sub_rect_height + vert_move_vec = base_plane.y * (sub_rect_height + vertical_separation) + vert_segs = [seg.move(vert_move_vec) for seg in scaled_segs] + for seg in scaled_segs + vert_segs: + final_faces.append(Face3D( + (seg.p1, seg.p2, seg.p2 + h_vec, seg.p1 + h_vec), base_plane)) + else: + h_vec = base_plane.y * sub_rect_height + for seg in scaled_segs: + final_faces.append(Face3D( + (seg.p1, seg.p2, seg.p2 + h_vec, seg.p1 + h_vec), base_plane)) + else: + # make a single sub-rectangle at an appropriate sill height + max_sill_h = parent_height * 0.99 - (target_area / (parent_base * 0.98)) + sill_vec = base_plane.y * sill_height if sill_height < max_sill_h \ + else base_plane.y * max_sill_h + seg_init = bottom_seg.move(sill_vec) + seg = seg_init.scale(0.98, seg_init.midpoint) + # find the maximum acceptable area for splitting the glazing vertically. + if vertical_separation != 0: + max_split_vert = parent_height - sill_height - \ + (target_area / (parent_base * 0.98)) - (0.02 * parent_height) + if vertical_separation < 0 or max_split_vert < 0: + vertical_separation = 0 + elif vertical_separation > max_split_vert: + vertical_separation = max_split_vert + # generate the vertices by 'extruding' along a window height vector. + if vertical_separation != 0: + sub_rect_height = (target_area / (parent_base * 0.98)) / 2 + h_vec = base_plane.y * sub_rect_height + vert_move_vec = base_plane.y * (sub_rect_height + vertical_separation) + vert_seg = seg.move(vert_move_vec) + final_faces = [] + for seg in [seg, vert_seg]: + final_faces.append(Face3D( + (seg.p1, seg.p2, seg.p2 + h_vec, seg.p1 + h_vec), base_plane)) + else: + h_vec = base_plane.y * (target_area / (parent_base * 0.98)) + final_faces = [Face3D((seg.p1, seg.p2, seg.p2 + h_vec, seg.p1 + h_vec), + base_plane)] + return final_faces
+ + +
+[docs] + @staticmethod + def sub_rects_from_rect_dimensions( + base_plane, parent_base, parent_height, sub_rect_height, sub_rect_width, + sill_height, horizontal_separation): + """Get a list of rectangular Face3D objects from dimensions and parameters. + + All of the resulting Face3D objects lie within a parent rectangle defined + by the parent_base, parent_height, and base_plane. + + Args: + base_plane: A Plane object in which the rectangle exists. + The origin of this plane will be the lower left corner of the + rectangle and the X and Y axes will form the sides. + parent_base: A number indicating the length of the base of the + parent rectangle. + parent_height: A number indicating the length of the height of the + parent rectangle. + sub_rect_height: A number for the target height of the output rectangles. + sub_rect_width: A number for the target width of the output rectangles. + sill_height: A number for the target height above the bottom edge of + the rectangle to start the sub-rectangles. If the sub_rect_height + is too large for the sill_height to fit within the rectangle, + the sub_rect_height will take precedence. + horizontal_separation: A number for the target separation between + individual sub-rectangle center lines. If this number is larger than + the parent rectangle base, only one sub-rectangle will be produced. + + Returns: + A list of Face3D objects for sub faces. + """ + # if sub_rect_height > parent_height, set it to just under parent_height + sub_rect_height = parent_height - 0.02 * parent_height if \ + sub_rect_height >= parent_height else sub_rect_height + # if sill_height is close to 0, set it to just above 0 + sill_hgt = 0.01 * parent_height if sill_height < 0.01 * parent_height \ + else sill_height + # adjust sill_hgt if sum of it and sub_rect_height > parent_height + if sub_rect_height + sill_hgt >= parent_height: + sill_hgt = parent_height - sub_rect_height - (parent_height * 0.01) + + # ensure that the horizontal_separation is always greater than sub_rect_width + if sub_rect_width >= horizontal_separation: + horizontal_separation = sub_rect_width * 1.02 + + # determine if the parameters should yield multiple sub-windows or just one + max_width_break_up = parent_base / 2 + num_div = round(parent_base / horizontal_separation) if \ + parent_base > horizontal_separation / 2 else 1 + # properties used throughout the computation of sub-rectangles + sill_vec = base_plane.y * sill_hgt + bottom_seg = LineSegment3D.from_sdl(base_plane.o, base_plane.x, parent_base) + + if sub_rect_width < max_width_break_up: + # determine the number of times that the rectangle should be subdivided + div_dist = parent_base / 2 if num_div == 1 else horizontal_separation + if num_div * sub_rect_width + (num_div - 1) * \ + (horizontal_separation - sub_rect_width) > parent_base: + num_div = math.floor(parent_base / horizontal_separation) + + # Get a segment in the center of the bottom + scale_fac = (div_dist * num_div) / parent_base + rect_seg = bottom_seg.scale(scale_fac, bottom_seg.point_at(0.5)) + rect_seg = rect_seg.move(sill_vec) + btm_div_pts = rect_seg.subdivide_evenly(num_div) + if len(btm_div_pts) == num_div: + btm_div_pts.append(rect_seg.p2) + + # divide up the rectangle into points on the bottom + btm_div_segs = tuple(LineSegment3D.from_end_points(pt, btm_div_pts[i + 1]) + for i, pt in enumerate(btm_div_pts[:-1])) + # scale the line segments along their center points + line_cent_pt = tuple(line.point_at(0.5) for line in btm_div_segs) + scale_factor = sub_rect_width / div_dist + btm_div_segs = tuple(line.scale(scale_factor, mid_pt) + for line, mid_pt in zip(btm_div_segs, line_cent_pt)) + # generate the vertices by 'extruding' along window height vector + h_vec = base_plane.y * sub_rect_height + final_faces = [Face3D((line.p2, line.p1, line.p1 + h_vec, line.p2 + h_vec), + base_plane) for line in btm_div_segs] + else: # make a single sub-rectangle at an appropriate sill height + if sub_rect_width >= parent_base: + sub_rect_width = parent_base * 0.98 + scale_fac = sub_rect_width / parent_base + rect_seg = bottom_seg.scale(scale_fac, bottom_seg.point_at(0.5)) + seg = rect_seg.move(sill_vec) + # generate the vertices by 'extruding' along window height vector + h_vec = base_plane.y * sub_rect_height + final_faces = [Face3D((seg.p2, seg.p1, seg.p1 + h_vec, seg.p2 + h_vec), + base_plane)] + return final_faces
+ + +
+[docs] + def coplanar_difference(self, faces, tolerance, angle_tolerance): + """Subtract one or more coplanar Face3D from this Face3D. + + Note that, when the faces are not coplanar or they do not overlap, a list + with only the original face will be returned. + + Args: + faces: A list of Face3D for which will be subtracted from this Face3D. + tolerance: The minimum difference between X, Y and Z values at which + vertices are considered distinct from one another. + angle_tolerance: The max angle in radians that the plane normals can + differ from one another in order for them to be considered coplanar. + + Returns: + A List of Face3D representing the original Face3D with the input faces + subtracted from it. + """ + # define the primary boolean polygon + prim_pl = self.plane + f1_poly = self.boundary_polygon2d + try: + f1_poly = f1_poly.remove_colinear_vertices(tolerance) + except AssertionError: # degenerate face input + return [self] + f1_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in f1_poly.vertices)] + if self.has_holes: + for hole in self.hole_polygon2d: + f1_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in hole.vertices)) + b_poly1 = pb.BooleanPolygon(f1_polys) + + # pre-process the Face3Ds to be intersected + relevant_b_polys = [] + for face2 in faces: + # test whether the faces are coplanar + if not prim_pl.is_coplanar_tolerance(face2.plane, tolerance, angle_tolerance): + continue + # test whether the two polygons have any overlap in 2D space + f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary)) + if f1_poly.polygon_relationship(f2_poly, tolerance) == -1: + continue + # snap the polygons to one another to avoid tolerance issues + try: + f2_poly = f2_poly.remove_colinear_vertices(tolerance) + except AssertionError: # degenerate faces input + continue + s2_poly = f1_poly.snap_to_polygon(f2_poly, tolerance) + # get BooleanPolygons of the two faces + f2_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in s2_poly.vertices)] + if face2.has_holes: + for hole in face2.holes: + h_pt2d = (prim_pl.xyz_to_xy(pt) for pt in hole) + f2_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in h_pt2d)) + b_poly2 = pb.BooleanPolygon(f2_polys) + relevant_b_polys.append(b_poly2) + + # if no relevant polygons were found, return self + if len(relevant_b_polys) == 0: + return [self] + + # loop through the boolean polygons and subtract them + int_tol = tolerance / 1000 + for b_poly2 in relevant_b_polys: + # subtract the boolean polygons + try: + b_poly1 = pb.difference(b_poly1, b_poly2, int_tol) + except Exception: + return [self] # typically a tolerance issue causing failure + + # rebuild the Face3D from the result of the subtraction + return Face3D._from_bool_poly(b_poly1, prim_pl, tolerance)
+ + +
+[docs] + @staticmethod + def coplanar_union(face1, face2, tolerance, angle_tolerance): + """Boolean Union two coplanar Face3D with one another. + + Args: + face1: A Face3D for the first face that will be unioned with the second face. + face2: A Face3D for the second face that will be unioned with the first face. + tolerance: The minimum difference between X, Y and Z values at which + vertices are considered distinct from one another. + angle_tolerance: The max angle in radians that the plane normals can + differ from one another in order for them to be considered coplanar. + + Returns: + A single Face3D for the Union of the two input Face3D. When the faces + are not coplanar or they do not overlap, None will be returned. + """ + # test whether the faces are coplanar + prim_pl = face1.plane + if not prim_pl.is_coplanar_tolerance(face2.plane, tolerance, angle_tolerance): + return None + # test whether the two polygons have any overlap in 2D space + f1_poly = face1.boundary_polygon2d + f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary)) + if f1_poly.polygon_relationship(f2_poly, tolerance) == -1: + return None + # snap the polygons to one another to avoid tolerance issues + try: + f1_poly = f1_poly.remove_colinear_vertices(tolerance) + f2_poly = f2_poly.remove_colinear_vertices(tolerance) + except AssertionError: # degenerate faces input + return None + s2_poly = f1_poly.snap_to_polygon(f2_poly, tolerance) + # get BooleanPolygons of the two faces + f1_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in f1_poly.vertices)] + f2_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in s2_poly.vertices)] + if face1.has_holes: + for hole in face1.hole_polygon2d: + f1_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in hole.vertices)) + if face2.has_holes: + for hole in face2.holes: + h_pt2d = (prim_pl.xyz_to_xy(pt) for pt in hole) + f2_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in h_pt2d)) + b_poly1 = pb.BooleanPolygon(f1_polys) + b_poly2 = pb.BooleanPolygon(f2_polys) + # union the two boolean polygons with one another + int_tol = tolerance / 1000 + try: + poly_result = pb.union(b_poly1, b_poly2, int_tol) + except Exception: + return None # typically a tolerance issue causing failure + # rebuild the Face3D from the results and return them + union_faces = Face3D._from_bool_poly(poly_result, prim_pl, tolerance) + return union_faces[0]
+ + +
+[docs] + @staticmethod + def coplanar_intersection(face1, face2, tolerance, angle_tolerance): + """Boolean Intersection two coplanar Face3D with one another. + + Args: + face1: A Face3D for the first face that will be intersected with + the second face. + face2: A Face3D for the second face that will be intersected with + the first face. + tolerance: The minimum difference between X, Y and Z values at which + vertices are considered distinct from one another. + angle_tolerance: The max angle in radians that the plane normals can + differ from one another in order for them to be considered coplanar. + + Returns: + A list of Face3D for the Intersection of the two input Face3D. + When the faces are not coplanar or they do not overlap, None will + be returned. + """ + # test whether the faces are coplanar + prim_pl = face1.plane + if not prim_pl.is_coplanar_tolerance(face2.plane, tolerance, angle_tolerance): + return None + # test whether the two polygons have any overlap in 2D space + f1_poly = face1.boundary_polygon2d + f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary)) + if f1_poly.polygon_relationship(f2_poly, tolerance) == -1: + return None + # snap the polygons to one another to avoid tolerance issues + try: + f1_poly = f1_poly.remove_colinear_vertices(tolerance) + f2_poly = f2_poly.remove_colinear_vertices(tolerance) + except AssertionError: # degenerate faces input + return None + s2_poly = f1_poly.snap_to_polygon(f2_poly, tolerance) + # get BooleanPolygons of the two faces + f1_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in f1_poly.vertices)] + f2_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in s2_poly.vertices)] + if face1.has_holes: + for hole in face1.hole_polygon2d: + f1_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in hole.vertices)) + if face2.has_holes: + for hole in face2.holes: + h_pt2d = (prim_pl.xyz_to_xy(pt) for pt in hole) + f2_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in h_pt2d)) + b_poly1 = pb.BooleanPolygon(f1_polys) + b_poly2 = pb.BooleanPolygon(f2_polys) + # intersect the two boolean polygons with one another + int_tol = tolerance / 1000 + try: + poly_result = pb.intersect(b_poly1, b_poly2, int_tol) + except Exception: + return None # typically a tolerance issue causing failure + # rebuild the Face3D from the results and return them + int_faces = Face3D._from_bool_poly(poly_result, prim_pl, tolerance) + return int_faces
+ + +
+[docs] + @staticmethod + def coplanar_split(face1, face2, tolerance, angle_tolerance): + """Split two coplanar Face3D with one another (ensuring matching overlapped area) + + When the faces are not coplanar or they do not overlap, the original + faces will be returned. + + Args: + face1: A Face3D for the first face that will be split with the second face. + face2: A Face3D for the second face that will be split with the first face. + tolerance: The minimum difference between X, Y and Z values at which + vertices are considered distinct from one another. + angle_tolerance: The max angle in radians that the plane normals can + differ from one another in order for them to be considered coplanar. + + Returns: + A tuple with two elements + + - face1_split: A list of Face3D for the split version of the input face1. + + - face2_split: A list of Face3D for the split version of the input face2. + """ + # test whether the faces are coplanar + prim_pl = face1.plane + if not prim_pl.is_coplanar_tolerance(face2.plane, tolerance, angle_tolerance): + return [face1], [face2] + # test whether the two polygons have any overlap in 2D space + f1_poly = face1.boundary_polygon2d + f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary)) + if f1_poly.polygon_relationship(f2_poly, tolerance) == -1: + return [face1], [face2] + # snap the polygons to one another to avoid tolerance issues + try: + f1_poly = f1_poly.remove_colinear_vertices(tolerance) + f2_poly = f2_poly.remove_colinear_vertices(tolerance) + except AssertionError: # degenerate faces input + return [face1], [face2] + s2_poly = f1_poly.snap_to_polygon(f2_poly, tolerance) + # get BooleanPolygons of the two faces + f1_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in f1_poly.vertices)] + f2_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in s2_poly.vertices)] + if face1.has_holes and face2.has_holes: # snap corresponding holes together + f1h_polys = face1.hole_polygon2d + f2h_polys = [Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in h_pts)) + for h_pts in face2.holes] + for f1hp in f1h_polys: + for hi, f2hp in enumerate(f2h_polys): + if f1hp.center.distance_to_point(f2hp.center) < tolerance: + f2h_polys[hi] = f1hp.snap_to_polygon(f2hp, tolerance) + for hole in f1h_polys: + f1_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in hole.vertices)) + for hole in f2h_polys: + f2_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in hole.vertices)) + elif face1.has_holes: + for hole in face1.hole_polygon2d: + f1_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in hole.vertices)) + elif face2.has_holes: + for hole in face2.holes: + h_pt2d = (prim_pl.xyz_to_xy(pt) for pt in hole) + f2_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in h_pt2d)) + b_poly1 = pb.BooleanPolygon(f1_polys) + b_poly2 = pb.BooleanPolygon(f2_polys) + # split the two boolean polygons with one another + int_tol = tolerance / 1000 + try: + int_result, poly1_result, poly2_result = pb.split(b_poly1, b_poly2, int_tol) + except Exception: + return [face1], [face2] # typically a tolerance issue causing failure + # rebuild the Face3D from the results and return them + int_faces = Face3D._from_bool_poly(int_result, prim_pl, tolerance) + poly1_faces = Face3D._from_bool_poly(poly1_result, prim_pl, tolerance) + poly2_faces = Face3D._from_bool_poly(poly2_result, prim_pl, tolerance) + face1_split = poly1_faces + int_faces + face2_split = poly2_faces + int_faces + return face1_split, face2_split
+ + +
+[docs] + @staticmethod + def coplanar_union_all(faces, tolerance, angle_tolerance): + """Boolean Union several coplanar Face3D together. + + Note that this method does not perform any check for whether the input + faces overlap before it performs the unioning operation. So it is + recommended that the Face3D.group_by_coplanar_overlap method be run + before using this method to union each group together. + + Args: + faces: A list of Face3D that will be unioned together. + tolerance: The minimum difference between X, Y and Z values at which + vertices are considered distinct from one another. + angle_tolerance: The max angle in radians that the plane normals can + differ from one another in order for them to be considered coplanar. + + Returns: + A list of Face3D for the Union of all the input Face3D. When the faces + are not coplanar, None will be returned. + """ + # test whether the faces are coplanar + prim_pl = faces[0].plane + for of in faces[1:]: + if not prim_pl.is_coplanar_tolerance(of.plane, tolerance, angle_tolerance): + return None + # convert all boundaries and holes to 2D space + hole_decoder = [False] + all_poly = [faces[0].boundary_polygon2d] + if faces[0].has_holes: + for hole in faces[0].hole_polygon2d: + all_poly.extend(hole) + hole_decoder.append(True) + for of in faces[1:]: + of_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in of.boundary)) + all_poly.append(of_poly) + hole_decoder.append(False) + if of.has_holes: + for hole in of.holes: + h_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in hole)) + all_poly.extend(h_poly) + hole_decoder.append(True) + # snap the polygons to one another to avoid tolerance issues + try: + all_poly = [ply.remove_colinear_vertices(tolerance) for ply in all_poly] + except AssertionError: # degenerate faces input + return None + all_poly = Polygon2D.snap_polygons(all_poly, tolerance) + # create BooleanPolygons of the faces + bool_polys = [] + prev_poly = None + for ply, is_hole in zip(all_poly, hole_decoder): + bool_pts = (pb.BooleanPoint(pt.x, pt.y) for pt in ply.vertices) + if not is_hole: + if prev_poly is not None: + bool_polys.append(pb.BooleanPolygon(prev_poly)) + prev_poly = [bool_pts] + else: + prev_poly.append(bool_pts) + bool_polys.append(pb.BooleanPolygon(prev_poly)) + # union the boolean polygons with one another + int_tol = tolerance / 100 + try: + poly_result = pb.union_all(bool_polys, int_tol) + except Exception: + return None # typically a tolerance issue causing failure + # rebuild the Face3D from the results and return them + union_faces = Face3D._from_bool_poly(poly_result, prim_pl, tolerance) + return union_faces
+ + + @staticmethod + def _from_bool_poly(bool_polygon, plane, tolerance=None): + """Get a list of Face3D from a BooleanPolygon. + + This method will automatically check whether any of the regions is meant + to be a hole within the others when it creates the Face3D. + + Args: + bool_polygon: A BooleanPolygon to be interpreted to Face3D. + plane: The Plane in which the resulting Face3Ds exist. + tolerance: An optional tolerance value to be used to remove + degenerate objects from the result. If None, the result may + contain degenerate objects. + """ + # serialize the BooleanPolygon into Polygon2D + polys = [] + for new_poly in bool_polygon.regions: + if len(new_poly) > 2: + poly = Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in new_poly)) + if tolerance is not None: + try: + poly = poly.remove_duplicate_vertices(tolerance) + polys.append(poly) + except AssertionError: + pass # degenerate polygon to be removed + else: + polys.append(poly) + if len(polys) == 0: + return [] + if len(polys) == 1: + verts_3d = tuple(plane.xy_to_xyz(pt) for pt in polys[0].vertices) + return [Face3D(verts_3d, plane)] + # sort the polygons by area and check if any are inside the others + polys.sort(key=lambda x: x.area, reverse=True) + poly_groups = [[polys[0]]] + for sub_poly in polys[1:]: + for i, pg in enumerate(poly_groups): + if pg[0].is_polygon_inside(sub_poly): # it's a hole + poly_groups[i].append(sub_poly) + break + else: # it's a separate Face3D + poly_groups.append([sub_poly]) + # convert all vertices to 3D and return the Face3D + face_3d = [] + for pg in poly_groups: + pg_3d = [] + for shp in pg: + pg_3d.append(tuple(plane.xy_to_xyz(pt) for pt in shp.vertices)) + face_3d.append(Face3D(pg_3d[0], plane, holes=pg_3d[1:])) + return face_3d + +
+[docs] + @staticmethod + def group_by_coplanar_overlap(faces, tolerance): + """Group coplanar Face3Ds depending on whether they overlap one another. + + This is useful as a pre-step before running Face3D.coplanar_union() + in order to assess whether union-ing is necessary and to ensure that + it is only performed among the necessary groups of faces. + + This method will return the minimal number of overlapping polygon groups + thanks to a recursive check of whether groups can be merged. + + Args: + faces: A list of Face3D to be grouped by their overlapping. + tolerance: The minimum distance from the edge of a neighboring Face3D + at which a point is considered to overlap with that Face3D. + + Returns: + A list of lists where each sub-list represents a group of Face3Ds + that all overlap with one another. + """ + # sort the faces by area to ensure larger ones grab smaller ones + faces = list(sorted(faces, key=lambda x: x.area, reverse=True)) + # create polygons for all of the faces + r_plane = faces[0].plane + polygons = [Polygon2D([r_plane.xyz_to_xy(pt) for pt in face.vertices]) + for face in faces] + + # loop through the polygons and check to see if it overlaps with the others + grouped_polys, grouped_faces = [[polygons[0]]], [[faces[0]]] + for poly, face in zip(polygons[1:], faces[1:]): + group_found = False + for poly_group, face_group in zip(grouped_polys, grouped_faces): + for oth_poly in poly_group: + if poly.polygon_relationship(oth_poly, tolerance) >= 0: + poly_group.append(poly) + face_group.append(face) + group_found = True + break + if group_found: + break + if not group_found: # the polygon does not overlap with any of the others + grouped_polys.append([poly]) # make a new group for the polygon + grouped_faces.append([face]) # make a new group for the face + + # if some groups were found, recursively merge groups together + old_group_len = len(polygons) + while len(grouped_polys) != old_group_len: + new_poly_groups, new_face_groups = grouped_polys[:], grouped_faces[:] + g_to_remove = [] + for i, group_1 in enumerate(grouped_polys): + try: + zip_obj = zip(grouped_polys[i + 1:], grouped_faces[i + 1:]) + for j, (group_2, f2) in enumerate(zip_obj): + if Polygon2D._groups_overlap(group_1, group_2, tolerance): + new_poly_groups[i] = new_poly_groups[i] + group_2 + new_face_groups[i] = new_face_groups[i] + f2 + g_to_remove.append(i + j + 1) + except IndexError: + pass # we have reached the end of the list of polygons + if len(g_to_remove) != 0: + g_to_remove = list(set(g_to_remove)) + g_to_remove.sort() + for ri in reversed(g_to_remove): + new_poly_groups.pop(ri) + new_face_groups.pop(ri) + old_group_len = len(grouped_polys) + grouped_polys = new_poly_groups + grouped_faces = new_face_groups + return grouped_faces
+ + +
+[docs] + @staticmethod + def join_coplanar_faces(faces, tolerance): + """Join a list of coplanar Face3Ds together to get as few as possible. + + Note that this method does not perform any boolean union operations on + the input faces. It will only join the objects together along shared edges. + + Args: + faces: A list of Face3D objects to be joined together. These should + all be coplanar but they do not need to have their colinear + vertices removed or be intersected for matching segments along + which they are joined. + tolerance: The maximum difference between values at which point vertices + are considered to be the same. + + Returns: + A list of Face3Ds for the minimum number joined together. + """ + # get polygons for the faces that all lie within the same plane + face_polys, base_plane = [], faces[0].plane + for fg in faces: + verts2d = tuple(base_plane.xyz_to_xy(_v) for _v in fg.boundary) + face_polys.append(Polygon2D(verts2d)) + if fg.has_holes: + for hole in fg.holes: + verts2d = tuple(base_plane.xyz_to_xy(_v) for _v in hole) + face_polys.append(Polygon2D(verts2d)) + + # remove colinear vertices + clean_face_polys = [] + for geo in face_polys: + try: + clean_face_polys.append(geo.remove_colinear_vertices(tolerance)) + except AssertionError: # degenerate geometry to ignore + pass + + # get the joined boundaries around the Polygon2D + joined_bounds = Polygon2D.joined_intersected_boundary( + clean_face_polys, tolerance) + + # convert the boundary polygons back to Face3D + if len(joined_bounds) == 1: # can be represented with a single Face3D + verts3d = tuple(base_plane.xy_to_xyz(_v) for _v in joined_bounds[0]) + return [Face3D(verts3d, plane=base_plane)] + else: # need to separate holes from distinct Face3Ds + bound_faces = [] + for poly in joined_bounds: + verts3d = tuple(base_plane.xy_to_xyz(_v) for _v in poly) + bound_faces.append(Face3D(verts3d, plane=base_plane)) + return Face3D.merge_faces_to_holes(bound_faces, tolerance)
+ + +
+[docs] + @staticmethod + def merge_faces_to_holes(faces, tolerance): + """Take of list of Face3Ds and merge any sub-faces into the others as holes. + + This is particularly useful when translating 2D Polygons back into a 3D + space and it is unknown whether certain polygons represent holes in the + others. + + Args: + faces: A list of Face3D which will be merged into fewer faces with + any sub-faces represented as holes. + tolerance: The tolerance to be used for evaluating sub-faces. + """ + # sort the faces by area and separate base face from the remaining + faces = sorted(faces, key=lambda x: x.area, reverse=True) + base_face = faces[0] + remain_faces = list(faces[1:]) + + # merge the smaller faces into the larger faces + merged_face3ds = [] + while len(remain_faces) > 0: + merged_face3ds.append( + Face3D._match_holes_to_face(base_face, remain_faces, tolerance)) + if len(remain_faces) > 1: + base_face = remain_faces[0] + del remain_faces[0] + elif len(remain_faces) == 1: # lone last Face3D + merged_face3ds.append(remain_faces[0]) + del remain_faces[0] + return merged_face3ds
+ + + @staticmethod + def _match_holes_to_face(base_face, other_faces, tol): + """Attempt to merge other faces into a base face as holes. + + Args: + base_face: A Face3D to serve as the base. + other_faces: A list of other Face3D objects to attempt to merge into + the base_face as a hole. This method will delete any faces + that are successfully merged into the output from this list. + tol: The tolerance to be used for evaluating sub-faces. + + Returns: + A Face3D which has holes in it if any of the other_faces is a valid + sub face. + """ + holes = [] + more_to_check = True + while more_to_check: + for i, r_face in enumerate(other_faces): + if base_face.is_sub_face(r_face, tol, 1): + holes.append(r_face) + del other_faces[i] + break + else: + more_to_check = False + if len(holes) == 0: + return base_face + else: + hole_verts = [hole.vertices for hole in holes] + return Face3D(base_face.vertices, base_face.plane, hole_verts) + +
+[docs] + def to_dict(self, include_plane=True, enforce_upper_left=False): + """Get Face3D as a dictionary. + + Args: + include_plane: Set to True to include the Face3D plane in the + dictionary, which will preserve the underlying orientation + of the face plane. Default True. + enforce_upper_left: Set to True to ensure that the boundary vertices all + start from the upper-left corner. This takes extra time to compute but + ensures that the vertices in the dictionary are directly usable in an + EnergyPlus simulations. Default: False. + """ + base = {'type': 'Face3D'} + if not enforce_upper_left: + base['boundary'] = [pt.to_array() for pt in self.boundary] + else: + base['boundary'] = [pt.to_array() for pt in + self._upper_left_counter_clockwise_boundary()] + if include_plane: + base['plane'] = self.plane.to_dict() + if self.has_holes: + base['holes'] = [[pt.to_array() for pt in hole] + for hole in self.holes] + return base
+ + +
+[docs] + def to_array(self): + """Get Face3D as a nested list of tuples where each sub-tuple represents loop. + + The first loop is always the outer boundary and successive loops represent + holes in the face (if they exist). Each sub-tuple is composed of tuples + that each have a length of 3 and denote 3D points that define the face. + """ + if self.has_holes: + return (tuple(pt.to_array() for pt in self.boundary),) + \ + tuple(tuple(pt.to_array() for pt in hole) for hole in self.holes) + else: + return (tuple(pt.to_array() for pt in self.boundary),)
+ + +
+[docs] + @staticmethod + def extract_all_from_stl(file_path): + """Get a list of Face3Ds imported from all of the triangles in an STL file. + + Args: + file_path: Path to an STL file as a text string. The STL file can be + in either ASCII or binary format. + """ + from ladybug_geometry.interop.stl import STL # avoid circular import + stl_obj = STL.from_file(file_path) + all_faces = [] + for verts, normal in zip(stl_obj.face_vertices, stl_obj.face_normals): + all_faces.append(Face3D(verts, plane=Plane(normal, verts[0]))) + return all_faces
+ + + def _check_vertices_input(self, vertices, loop_name='boundary'): + if not isinstance(vertices, tuple): + vertices = tuple(vertices) + assert len(vertices) >= 3, 'There must be at least 3 vertices for a Face3D {}.' \ + ' Got {}'.format(loop_name, len(vertices)) + for vert in vertices: + assert isinstance(vert, Point3D), \ + 'Expected Point3D for Face3D {} vertex. Got {}.'.format( + loop_name, type(vert)) + return vertices + + def _check_number_mesh_grid(self, input, name): + assert isinstance(input, (float, int)), '{} for Face3D.get_mesh_grid' \ + ' must be a number. Got {}.'.format(name, type(input)) + + def _move(self, vertices, mov_vec): + return tuple(pt.move(mov_vec) for pt in vertices) + + def _rotate(self, vertices, axis, angle, origin): + return tuple(pt.rotate(axis, angle, origin) for pt in vertices) + + def _rotate_xy(self, vertices, angle, origin): + return tuple(pt.rotate_xy(angle, origin) for pt in vertices) + + def _reflect(self, vertices, normal, origin): + return tuple(pt.reflect(normal, origin) for pt in reversed(vertices)) + + def _scale(self, vertices, factor, origin): + if origin is None: + return tuple( + Point3D(pt.x * factor, pt.y * factor, pt.z * factor) + for pt in vertices) + else: + return tuple(pt.scale(factor, origin) for pt in vertices) + + def _face_transform(self, verts, plane): + """Transform face in a way that transfers properties and avoids checks.""" + _new_face = Face3D(verts, plane, enforce_right_hand=False) + self._transfer_properties(_new_face) + _new_face._polygon2d = self._polygon2d + _new_face._mesh2d = self._mesh2d + return _new_face + + def _face_transform_reflect(self, verts, plane): + """Reflect face in a way that transfers properties and avoids checks.""" + _new_face = Face3D(verts, plane, enforce_right_hand=False) + self._transfer_properties(_new_face) + return _new_face + + def _face_transform_scale(self, verts, plane, factor): + """Scale face in a way that transfers properties and avoids checks.""" + _new_face = Face3D(verts, plane, enforce_right_hand=False) + self._transfer_properties_scale(_new_face, factor) + return _new_face + + def _transfer_properties(self, new_face): + """Transfer properties from this face to a new face. + + This is used by the transform methods that don't alter the relationship of + face vertices to one another (move, rotate, reflect). + """ + new_face._perimeter = self._perimeter + new_face._area = self._area + new_face._is_convex = self._is_convex + new_face._is_self_intersecting = self._is_self_intersecting + + def _transfer_properties_scale(self, new_face, factor): + """Transfer properties from this face to a new face. + + This is used by the methods that scale the face. + """ + new_face._is_convex = self._is_convex + new_face._is_self_intersecting = self._is_self_intersecting + if self._perimeter is not None: + new_face._perimeter = self._perimeter * factor + if self._area is not None: + new_face._area = self._area * factor ** 2 + + def _calculate_min_max(self): + """Calculate maximum and minimum Point3D for this object.""" + min_pt = [self.boundary[0].x, self.boundary[0].y, self.boundary[0].z] + max_pt = [self.boundary[0].x, self.boundary[0].y, self.boundary[0].z] + + for v in self.boundary[1:]: + if v.x < min_pt[0]: + min_pt[0] = v.x + elif v.x > max_pt[0]: + max_pt[0] = v.x + if v.y < min_pt[1]: + min_pt[1] = v.y + elif v.y > max_pt[1]: + max_pt[1] = v.y + if v.z < min_pt[2]: + min_pt[2] = v.z + elif v.z > max_pt[2]: + max_pt[2] = v.z + + self._min = Point3D(min_pt[0], min_pt[1], min_pt[2]) + self._max = Point3D(max_pt[0], max_pt[1], max_pt[2]) + + def _remove_colinear(self, pts_3d, pts_2d, tolerance): + """Remove colinear vertices from a list of Point2D. + + This method determines co-linearity by checking whether the area of the + triangle formed by 3 vertices is less than the tolerance. + """ + new_vertices = [] # list to hold the new vertices + skip = 0 # track the number of vertices being skipped/removed + first_skip, is_first, = 0, True # track the number skipped from first vertex + # loop through vertices and remove all cases of colinear verts + for i, _v in enumerate(pts_2d): + _v2, _v1 = pts_2d[i - 2 - skip], pts_2d[i - 1] + _a = _v2.determinant(_v1) + _v1.determinant(_v) + _v.determinant(_v2) + b_dist = _v.distance_to_point(_v2) + b_dist = tolerance if b_dist < tolerance else b_dist + tri_tol = (b_dist * tolerance) / 2 # area of triangle with tolerance height + if abs(_a) >= tri_tol: # triangle area > area tolerance; not colinear + new_vertices.append(pts_3d[i - 1]) + skip = 0 + if is_first: + is_first = False + first_skip = i - 1 + else: # colinear point to be removed + skip += 1 + # catch case of last few vertices being equal but distinct from first point + if skip != 0 and first_skip != -1: + assert abs(-2 - skip) <= len(pts_2d), \ + 'There must be at least 3 vertices for a Face3D.' + _v2, _v1, _v = pts_2d[-2 - skip], pts_2d[-1], pts_2d[first_skip] + _a = _v2.determinant(_v1) + _v1.determinant(_v) + _v.determinant(_v2) + b_dist = _v.distance_to_point(_v2) + b_dist = tolerance if b_dist < tolerance else b_dist + tri_tol = (b_dist * tolerance) / 2 # area of triangle with tolerance height + if abs(_a) >= tri_tol: # triangle area > area tolerance; not colinear + new_vertices.append(pts_3d[-1]) + return new_vertices + + def _is_sub_face(self, face): + """Check if a face is a sub-face of this face, bypassing coplanar check. + + Args: + face: Another face for which sub-face equivalency will be tested. + """ + verts2d = tuple(self.plane.xyz_to_xy(_v) for _v in face.vertices) + sub_poly = Polygon2D(verts2d) + + if not self.has_holes: + return self.polygon2d.is_polygon_inside(sub_poly) + else: + if not self.boundary_polygon2d.is_polygon_inside(sub_poly): + return False + for hole_poly in self.hole_polygon2d: + if not hole_poly.is_polygon_outside(sub_poly): + return False + return True + + def _vertices_between_points(self, start_pt, end_pt, tolerance): + """Get the vertices between a start and end point. + + This method is used by the extract_rectangle method. + """ + new_verts = [start_pt] + vert_ind = self.vertices.index(start_pt) + found_other = False + while found_other is False: + vert_ind -= 1 + new_verts.append(self[vert_ind]) + if self[vert_ind].is_equivalent(end_pt, tolerance): + found_other = True + return new_verts + + def _diagonal_along_self(self, direction_vector, tolerance): + """Get the diagonal oriented along this face and always starts on the left.""" + tol_pt = Vector3D(1.0e-7, 1.0e-7, 1.0e-7) # closer than float tolerance + diagonal = LineSegment3D.from_end_points(self.min + tol_pt, self.max - tol_pt) + # invert the diagonal XY if it is not oriented with the face plane + if self._plane.distance_to_point(diagonal.p) > tolerance: + start = Point3D(diagonal.p1.x, diagonal.p2.y, diagonal.p1.z) + end = Point3D(diagonal.p2.x, diagonal.p1.y, diagonal.p2.z) + diagonal = LineSegment3D.from_end_points(start, end) + # flip if there's a horizontal direction_vector to ensure always starts on left + if direction_vector.x != 0 and self.normal.y > 0: + diagonal = diagonal.flip() + return diagonal + + def _get_fin_extrusion_vector(self, depth, angle, contour_vector): + """Get the vector with which to extrude fins.""" + extru_vec = self.plane.n * depth + if angle != 0: + # interpret the complement of the 2D contour_vector into a 3D axis + cont_vec_complement = Vector2D(contour_vector.y, -contour_vector.x) + ref_plane = Plane(self._plane.n, Point3D(0, 0, 0), self._plane.x) + if ref_plane.y.z < 0: + ref_plane = ref_plane.rotate(ref_plane.n, math.pi, ref_plane.o) + axis = ref_plane.xy_to_xyz(cont_vec_complement).normalize() + # rotate the extrusion vector around the axis + extru_vec = extru_vec.rotate(axis, angle) + return extru_vec + + def _get_extrusion_fins(self, contours, extru_vec, offset): + """Get fins from the contours and extrusion vector.""" + if offset != 0: + off_vec = self.plane.n * offset + contours = tuple(seg.move(off_vec) for seg in contours) + return tuple(Face3D.from_extrusion(seg, extru_vec) for seg in contours) + + def _split_with_rectangle(self, edge_1, edge_2, tolerance): + """Split this shape using two parallel edges of the face. + + Result will be None if no rectangle can be obtained. + + Returns: + rectangle_points: A tuple of 4 points that make the rectangle. + other_faces: A list of faces for the other parts of this Face that + are not a part of the rectangle. + """ + # compute the 4 points defining the rectangle + close_pt_1 = closest_point3d_on_line3d(edge_1.p1, edge_2) + close_pt_2 = closest_point3d_on_line3d(edge_2.p2, edge_1) + close_pt_3 = closest_point3d_on_line3d(edge_1.p2, edge_2) + close_pt_4 = closest_point3d_on_line3d(edge_2.p1, edge_1) + + # check that there is overlap between the top and bottom curves + if close_pt_1.is_equivalent(edge_2.p1, tolerance) or \ + close_pt_3.is_equivalent(edge_2.p2, tolerance): + return None + + # check that the two sides of the rectangle are inside the polygon. + mid_pt_1 = self.plane.xyz_to_xy( + LineSegment3D.from_end_points(close_pt_1, close_pt_2).midpoint) + mid_pt_2 = self.plane.xyz_to_xy( + LineSegment3D.from_end_points(close_pt_3, close_pt_4).midpoint) + if self.polygon2d.point_relationship(mid_pt_1, tolerance) == -1 or \ + self.polygon2d.point_relationship(mid_pt_2, tolerance) == -1: + return None + + # get extra faces outside of the rectangle + other_faces = [] + edge_pts_1 = self._vertices_between_points(edge_1.p1, edge_2.p2, tolerance) + if close_pt_1.is_equivalent(edge_2.p2, tolerance) is False: + edge_pts_1.append(close_pt_1) + other_faces.append(Face3D(edge_pts_1, self.plane)) + elif close_pt_2.is_equivalent(edge_1.p1, tolerance) is False: + edge_pts_1.append(close_pt_2) + other_faces.append(Face3D(edge_pts_1, self.plane)) + elif len(edge_pts_1) > 2: + other_faces.append(Face3D(edge_pts_1, self.plane)) + + edge_pts_2 = self._vertices_between_points(edge_2.p1, edge_1.p2, tolerance) + if close_pt_3.is_equivalent(edge_2.p1, tolerance) is False: + edge_pts_2.append(close_pt_3) + other_faces.append(Face3D(edge_pts_2, self.plane)) + elif close_pt_4.is_equivalent(edge_1.p2, tolerance) is False: + edge_pts_2.append(close_pt_4) + other_faces.append(Face3D(edge_pts_2, self.plane)) + elif len(edge_pts_2) > 2: + other_faces.append(Face3D(edge_pts_2, self.plane)) + + # check that any new faces are not self intersecting + for new_face in other_faces: + if new_face.is_self_intersecting: + return None + + # return the rectangle edges and the extra faces + return (close_pt_1, close_pt_2, close_pt_3, close_pt_4), other_faces + + def _point_on_face(self, tolerance): + """Get a point that is always reliably on this face. + + The point will be close to the edge of the Face but it will always + be inside its boundary for all concave and holed geometries. Furthermore, + it is relatively fast compared with computing the pole_of_inaccessibility. + """ + try: + face = self.remove_colinear_vertices(tolerance) + move_vec = self._inward_pointing_vec(face) + except (AssertionError, ZeroDivisionError): # zero area Face3D; use center + return self.center + + move_vec = move_vec * (tolerance + 0.00001) + point_on_face = face.boundary[0] + move_vec + vert2d = face.plane.xyz_to_xy(point_on_face) + if not face.polygon2d.is_point_inside(vert2d): + point_on_face = face.boundary[0] - move_vec + return point_on_face + + def _upper_oriented_plane(self): + """Get a version of this Face3D's plane where Y is oriented towards positive Z. + + If the Face3D is horizontal, the plane will be the World XY. + """ + if self._plane.n.z == 1 or self._plane.n.z == -1: # no vertex is above another + ref_plane = Plane(self._plane.n, self._plane.o, Vector3D(1, 0, 0)) + else: + proj_y = Vector3D(0, 0, 1).project(self._plane.n) + proj_x = proj_y.rotate(self._plane.n, math.pi / -2) + ref_plane = Plane(self._plane.n, self._plane.o, proj_x) + return ref_plane + + def _corner_point(self, x_corner='min', y_corner='min'): + """Get a Point3D that is in a particular corner of this Face3D. + + Args: + x_corner: Either "min" or "max" depending on the desired corner. + y_corner: Either "min" or "max" depending on the desired corner. + """ + # get a correctly-oriented polygon + ref_plane = self._upper_oriented_plane() + polygon = Polygon2D(tuple(ref_plane.xyz_to_xy(v) for v in self._boundary)) + # sort points so that they start with the correct corner + x_pt = getattr(polygon, x_corner) + y_pt = getattr(polygon, y_corner) + return ref_plane.xy_to_xyz(Point2D(x_pt.x, y_pt.y)) + + def _corner_point_and_polygon(self, points_3d, x_corner='min', y_corner='min'): + """Get a Point2D and corresponding Polygon in a particular corner of this Face3D. + + Args: + points_3d: A list of Point3Ds for the output Polygon. + x_corner: Either "min" or "max" depending on the desired corner. + y_corner: Either "min" or "max" depending on the desired corner. + """ + if self.is_horizontal(0.01): # EnergyPlus tolerance + polygon = Polygon2D(tuple(Point2D(v.x, v.y) for v in points_3d)) + if self._plane.n.z < 0: # flip the direction of what counts as "right" + x_corner = 'max' if x_corner == 'min' else 'min' + x_pt = getattr(self, x_corner) + y_pt = getattr(self, y_corner) + else: + # get a 2d polygon in the face plane that has a positive Y axis. + proj_y = Vector3D(0, 0, 1).project(self._plane.n) + proj_x = proj_y.rotate(self._plane.n, math.pi / -2) + ref_plane = Plane(self._plane.n, self._plane.o, proj_x) + polygon = Polygon2D(tuple(ref_plane.xyz_to_xy(v) for v in points_3d)) + x_pt = getattr(polygon, x_corner) + y_pt = getattr(polygon, y_corner) + return Point2D(x_pt.x, y_pt.y), polygon + + def _counter_clockwise_verts(self, polygon): + """Get aligned lists of counter-clockwise 2D and 3D vertices.""" + if self.is_clockwise: + return tuple(reversed(self.vertices)), tuple(reversed(polygon.vertices)) + else: + return self.vertices, polygon.vertices + + def _counter_clockwise_bound(self, polygon): + """Get aligned lists of counter-clockwise 2D and 3D vertices.""" + if self.is_clockwise: + return tuple(reversed(self.boundary)), tuple(reversed(polygon.vertices)) + else: + return self.boundary, polygon.vertices + + def _upper_left_counter_clockwise_boundary(self): + """Get this face's boundary starting from upper left and moving counterclockwise. + + Horizontal faces will treat the positive Y axis as up. All other faces + treat the positive Z axis as up. + + Unlike the upper_left_counter_clockwise_vertices property, this property + does not include any holes in the Face3D. + """ + corner_pt, polygon = self._corner_point_and_polygon(self._boundary, 'min', 'max') + if self.is_clockwise: + verts3d, verts2d = \ + tuple(reversed(self.boundary)), tuple(reversed(polygon.vertices)) + else: + verts3d, verts2d = self.boundary, polygon.vertices + return self._corner_pt_verts(corner_pt, verts3d, verts2d) + + @staticmethod + def _inward_pointing_vec(face): + """Get a unit vector pointing inward/outward from the first vertex of a face.""" + v1 = face.boundary[-1] - face.boundary[0] + v2 = face.boundary[1] - face.boundary[0] + if v1.angle(v2) == math.pi: # colinear vertices; prevent averaging to zero + return v1.rotate(face.normal, math.pi / 2).normalize() + else: # average the two edge vectors together + avg_coords = ((v1.x + v2.x) / 2), ((v1.y + v2.y) / 2), ((v1.z + v2.z) / 2) + return Vector3D(*avg_coords).normalize() + + @staticmethod + def _plane_from_vertices(verts): + """Get a plane from a list of vertices. + + Args: + verts: The vertices to be used to extract the normal. + """ + try: + # walk around the shape and get cross products + cprods, base_vert = [], verts[0] + for i in range(len(verts) - 2): + verts_3 = (base_vert, verts[i + 1], verts[i + 2]) + cprods.append(Face3D._normal_from_3pts(*verts_3)) + # sum together the cross products + normal = [0, 0, 0] + for cprodx in cprods: + normal[0] += cprodx[0] + normal[1] += cprodx[1] + normal[2] += cprodx[2] + # normalize the vector + if normal != [0, 0, 0]: + ds = math.sqrt(normal[0] ** 2 + normal[1] ** 2 + normal[2] ** 2) + normal_vec = Vector3D(normal[0] / ds, normal[1] / ds, normal[2] / ds) + else: # zero area Face3D; default to positive Z axis + normal_vec = Vector3D(0, 0, 1) + except Exception as e: + raise ValueError('Incorrect vertices input for Face3D:\n\t{}'.format(e)) + return Plane(normal_vec, verts[0]) + + @staticmethod + def _normal_from_3pts(pt1, pt2, pt3): + """Get a tuple representing a normal vector from 3 vertices. + + The vector will have a magnitude of 0 if vertices are colinear. + This method effectively performs the cross product of two unit vectors but + the ladybug_geometry objects are not used in order to remove assertions + and increase speed. + """ + # get two vectors for the two edges the 3 points form + v1 = (pt2.x - pt1.x, pt2.y - pt1.y, pt2.z - pt1.z) + v2 = (pt3.x - pt1.x, pt3.y - pt1.y, pt3.z - pt1.z) + # get the cross product of the two edge vectors + return (v1[1] * v2[2] - v1[2] * v2[1], + -v1[0] * v2[2] + v1[2] * v2[0], + v1[0] * v2[1] - v1[1] * v2[0]) + + @staticmethod + def _corner_pt_verts(corner_pt, verts3d, verts2d): + """Get verts3d starting from the one closes to the corner_pt.""" + first_pt_index = 0 + min_dist = verts2d[0].distance_to_point(corner_pt) + for pt_index, pt in enumerate(verts2d[1:]): + new_dist = pt.distance_to_point(corner_pt) + if new_dist < min_dist: + first_pt_index = pt_index + 1 + min_dist = new_dist + if first_pt_index != 0: + verts3d = verts3d[first_pt_index:] + verts3d[:first_pt_index] + return verts3d + + def __copy__(self): + _new_face = Face3D(self.vertices, self.plane) + self._transfer_properties(_new_face) + _new_face._holes = self._holes + _new_face._polygon2d = self._polygon2d + _new_face._mesh2d = self._mesh2d + _new_face._mesh3d = self._mesh3d + return _new_face + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return tuple(hash(pt) for pt in self._vertices) + (hash(self._plane),) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Face3D) and self.__key() == other.__key() + + def __repr__(self): + return 'Face3D ({} vertices)'.format(len(self))
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/line.html b/docs/_modules/ladybug_geometry/geometry3d/line.html new file mode 100644 index 00000000..e337b77e --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/line.html @@ -0,0 +1,1506 @@ + + + + + + + ladybug_geometry.geometry3d.line — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.line

+# coding=utf-8
+"""3D Line Segment"""
+from __future__ import division
+
+from .pointvector import Point3D, Vector3D
+from ._1d import Base1DIn3D
+
+
+
+[docs] +class LineSegment3D(Base1DIn3D): + """3D line segment object. + + Args: + p: A Point3D representing the first point of the line segment. + v: A Vector3D representing the vector to the second point. + + Properties: + * p + * v + * p1 + * p2 + * min + * max + * center + * midpoint + * endpoints + * length + * vertices + """ + __slots__ = () + + def __init__(self, p, v): + """Initialize LineSegment3D.""" + Base1DIn3D.__init__(self, p, v) + +
+[docs] + @classmethod + def from_end_points(cls, p1, p2): + """Initialize a line segment from a start point and and end point. + + Args: + p1: A Point3D representing the first point of the line segment. + p2: A Point3D representing the second point of the line segment. + """ + return cls(p1, p2 - p1)
+ + +
+[docs] + @classmethod + def from_sdl(cls, s, d, length): + """Initialize a line segment from a start point, direction, and length. + + Args: + s: A Point3D representing the start point of the line segment. + d: A Vector3D representing the direction of the line segment. + length: A number representing the length of the line segment. + """ + return cls(s, d * length / d.magnitude)
+ + +
+[docs] + @classmethod + def from_array(cls, line_array): + """ Create a LineSegment3D from a nested array of two endpoint coordinates. + + Args: + line_array: Nested tuples ((pt1.x, pt1.y, pt.z), (pt2.x, pt2.y, pt.z)), + where pt1 and pt2 represent the endpoints of the line segment. + """ + return LineSegment3D.from_end_points(*tuple(Point3D(*pt) for pt in line_array))
+ + +
+[docs] + @classmethod + def from_line_segment2d(cls, line2d, z=0): + """Initialize a new LineSegment3D from an LineSegment2D and a z value. + + Args: + line2d: A LineSegment2D to be used to generate the LineSegment3D. + z: A number for the Z coordinate value of the line. + """ + base_p = Point3D(line2d.p.x, line2d.p.y, z) + base_v = Vector3D(line2d.v.x, line2d.v.y, 0) + return cls(base_p, base_v)
+ + + @property + def p1(self): + """First point (same as p).""" + return self.p + + @property + def p2(self): + """Second point.""" + return Point3D(self.p.x + self.v.x, self.p.y + self.v.y, self.p.z + self.v.z) + + @property + def midpoint(self): + """Midpoint.""" + return self.point_at(0.5) + + @property + def endpoints(self): + """Tuple of endpoints """ + return (self.p1, self.p2) + + @property + def length(self): + """The length of the line segment.""" + return self.v.magnitude + + @property + def vertices(self): + """Tuple of both vertices in this object.""" + return (self.p1, self.p2) + +
+[docs] + def is_horizontal(self, tolerance): + """Test whether this line segment is horizontal within a certain tolerance. + + Args: + tolerance: The maximum difference between the z values of the start and + end coordinates at which the line segment is considered horizontal. + """ + return abs(self.v.z) <= tolerance
+ + +
+[docs] + def is_vertical(self, tolerance): + """Test whether this line segment is vertical within a certain tolerance. + + Args: + tolerance: The maximum difference between the x and y values of the start + and end coordinates at which the line segment is considered horizontal. + """ + return abs(self.v.x) <= tolerance and abs(self.v.y) <= tolerance
+ + +
+[docs] + def flip(self): + """Get a copy of this line segment that is flipped.""" + return LineSegment3D(self.p2, self.v.reverse())
+ + +
+[docs] + def move(self, moving_vec): + """Get a line segment that has been moved along a vector. + + Args: + moving_vec: A Vector3D with the direction and distance to move the ray. + """ + return LineSegment3D(self.p.move(moving_vec), self.v)
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate a line segment by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + return LineSegment3D(self.p.rotate(axis, angle, origin), + self.v.rotate(axis, angle))
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a line segment rotated counterclockwise in the XY plane by a certain angle. + + Args: + angle: An angle in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + return LineSegment3D(self.p.rotate_xy(angle, origin), + self.v.rotate_xy(angle))
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a line segment reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the line segment will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin from which to reflect. + """ + return LineSegment3D(self.p.reflect(normal, origin), self.v.reflect(normal))
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a line segment by a factor from an origin point. + + Args: + factor: A number representing how much the line segment should be scaled. + origin: A Point3D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + return LineSegment3D(self.p.scale(factor, origin), self.v * factor)
+ + +
+[docs] + def subdivide(self, distances): + """Get Point3D values along the line that subdivide it based on input distances. + + Args: + distances: A list of distances along the line at which to subdivide it. + This can also be a single number that will be repeated until the + end of the line. + """ + if isinstance(distances, (float, int)): + distances = [distances] + # this assert prevents the while loop from being infinite + assert sum(distances) > 0, 'Segment subdivisions must be greater than 0' + line_length = self.length + dist = distances[0] + index = 0 + sub_pts = [self.p] + while dist < line_length: + sub_pts.append(self.point_at_length(dist)) + if index < len(distances) - 1: + index += 1 + dist += distances[index] + sub_pts.append(self.p2) + return sub_pts
+ + +
+[docs] + def subdivide_evenly(self, number): + """Get Point3D values along the line that divide it into evenly-spaced segments. + + Args: + number: Integer for the number of segments into which the line will + be divided. + """ + # this assert prevents the while loop from being infinite + assert number > 0, 'Segment subdivisions must be greater than 0' + interval = 1 / number + parameter = interval + sub_pts = [self.p] + while parameter <= 1: + sub_pts.append(self.point_at(parameter)) + parameter += interval + if len(sub_pts) != number + 1: # tolerance issue with last point + sub_pts.append(self.p2) + return sub_pts
+ + +
+[docs] + def point_at(self, parameter): + """Get a Point3D at a given fraction along the line segment. + + Args: + parameter: The fraction between the start and end point where the + desired point lies. For example, 0.5 will yield the midpoint. + """ + return self.p + self.v * parameter
+ + +
+[docs] + def point_at_length(self, length): + """Get a Point3D at a given distance along the line segment. + + Args: + length: The distance along the line from the start point where the + desired point lies. + """ + return self.p + self.v * (length / self.length)
+ + +
+[docs] + def split_with_plane(self, plane): + """Split this LineSegment3D in 2 smaller LineSegment3Ds using a Plane. + + Args: + plane: A Plane that will be used to split this line segment. + + Returns: + A list of two LineSegment3D objects if the split was successful. + Will be a list with 1 LineSegment3D if no intersection exists. + """ + _plane_int = self.intersect_plane(plane) + if _plane_int is not None: + return [LineSegment3D.from_end_points(self.p1, _plane_int), + LineSegment3D.from_end_points(_plane_int, self.p2)] + return [self]
+ + +
+[docs] + def to_dict(self): + """Get LineSegment3D as a dictionary.""" + base = Base1DIn3D.to_dict(self) + base['type'] = 'LineSegment3D' + return base
+ + +
+[docs] + def to_array(self): + """ A nested list representing the two line endpoint coordinates.""" + return (self.p1.to_array(), self.p2.to_array())
+ + + def _u_in(self, u): + return u >= 0.0 and u <= 1.0 + + def __abs__(self): + return abs(self.v) + + def __copy__(self): + return LineSegment3D(self.p, self.v) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (hash(self.p), hash(self.v)) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, LineSegment3D) and self.__key() == other.__key() + + def __repr__(self): + return 'LineSegment3D (<%.2f, %.2f, %.2f> to <%.2f, %.2f, %.2f>)' % \ + (self.p.x, self.p.y, self.p.z, + self.p.x + self.v.x, self.p.y + self.v.y, self.p.z + self.v.z)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/mesh.html b/docs/_modules/ladybug_geometry/geometry3d/mesh.html new file mode 100644 index 00000000..66daf7f1 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/mesh.html @@ -0,0 +1,1996 @@ + + + + + + + ladybug_geometry.geometry3d.mesh — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.mesh

+# coding=utf-8
+"""3D Mesh"""
+from __future__ import division
+
+from .._mesh import MeshBase
+from ..geometry2d.mesh import Mesh2D
+from .pointvector import Point3D, Vector3D
+from .line import LineSegment3D
+from .polyline import Polyline3D
+from .plane import Plane
+
+try:
+    from itertools import izip as zip  # python 2
+except ImportError:
+    xrange = range  # python 3
+
+
+
+[docs] +class Mesh3D(MeshBase): + """3D Mesh object. + + Args: + vertices: A list or tuple of Point3D objects for vertices. + faces: A list of tuples with each tuple having either 3 or 4 integers. + These integers correspond to indices within the list of vertices. + colors: An optional list of colors that correspond to either the faces + of the mesh or the vertices of the mesh. Default is None. + + Properties: + * vertices + * faces + * colors + * is_color_by_face + * min + * max + * center + * area + * face_areas + * face_centroids + * face_area_centroids + * face_vertices + * face_normals + * vertex_normals + * vertex_connected_faces + * face_edges + * edges + * naked_edges + * internal_edges + * non_manifold_edges + """ + __slots__ = ('_min', '_max', '_center', '_face_normals', '_vertex_normals') + + def __init__(self, vertices, faces, colors=None): + """Initialize Mesh3D.""" + self._vertices = self._check_vertices_input(vertices) + self._faces = self._check_faces_input(faces) + + self._is_color_by_face = False # default if colors is None + self.colors = colors + self._min = None + self._max = None + self._center = None + self._area = None + self._face_areas = None + self._face_centroids = None + self._face_area_centroids = None + self._face_normals = None + self._vertex_normals = None + self._vertex_connected_faces = None + self._edge_indices = None + self._edge_types = None + self._edges = None + self._naked_edges = None + self._internal_edges = None + self._non_manifold_edges = None + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Mesh3D from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "type": "Mesh3D", + "vertices": [(0, 0, 0), (10, 0, 0), (0, 10, 0)], + "faces": [(0, 1, 2)], + "colors": [{"r": 255, "g": 0, "b": 0}] + } + """ + colors = None + if 'colors' in data and data['colors'] is not None and len(data['colors']) != 0: + try: + from ladybug.color import Color + except ImportError: + raise ImportError('Colors are specified in input Mesh2D dictionary ' + 'but failed to import ladybug.color') + colors = tuple(Color.from_dict(col) for col in data['colors']) + fcs = tuple(tuple(f) for f in data['faces']) # cast to immutable type + return cls(tuple(Point3D.from_array(pt) for pt in data['vertices']), fcs, colors)
+ + +
+[docs] + @classmethod + def from_face_vertices(cls, faces, purge=True): + """Create a mesh from a list of faces with each face defined by Point3Ds. + + Args: + faces: A list of faces with each face defined as a list of 3 or 4 Point3D. + purge: A boolean to indicate if duplicate vertices should be shared between + faces. Default is True to purge duplicate vertices, which can be slow + for large lists of faces but results in a higher-quality mesh with + a smaller size in memory. Note that vertices are only considered + duplicate if the coordinate values are equal to one another + within floating point tolerance. To remove duplicate vertices + within a specified tolerance other than floating point, the + from_purged_face_vertices method should be used instead. + """ + vertices, face_collector = cls._interpret_input_from_face_vertices(faces, purge) + return cls(tuple(vertices), tuple(face_collector))
+ + +
+[docs] + @classmethod + def from_purged_face_vertices(cls, faces, tolerance): + """Create a mesh from a list of faces with each face defined by Point3Ds. + + This method is slower than 'from_face_vertices' but will result in a mesh + with fewer vertices and a smaller size in memory. This method is similar to + using the 'purge' option in 'from_face_vertices' but will result in more shared + vertices since it uses a tolerance to check equivalent vertices rather than + comparing within floating point tolerance. + + Args: + faces: A list of faces with each face defined as a list of 3 or 4 Point3D. + tolerance: A number for the minimum difference between coordinate + values at which point vertices are considered equal to one another. + """ + vertices, faces = cls._interpret_input_from_face_vertices_with_tolerance( + faces, tolerance) + return cls(tuple(vertices), tuple(faces))
+ + +
+[docs] + @classmethod + def from_mesh2d(cls, mesh_2d, plane=None): + """Create a Mesh3D from a Mesh2D and a Plane in which the mesh exists. + + Args: + mesh_2d: A Mesh2D object. + plane: A Plane object to represent the plane in which the Mesh2D exists + within 3D space. If None, the WorldXY plane will be used. + """ + assert isinstance(mesh_2d, Mesh2D), 'Expected Mesh2D for from_mesh_2d. ' \ + 'Got {}.'.format(type(mesh_2d)) + if plane is None: + return cls(tuple(Point3D(pt.x, pt.y, 0) for pt in mesh_2d.vertices), + mesh_2d.faces, mesh_2d.colors) + else: + assert isinstance(plane, Plane), 'Expected Plane. Got {}'.format(type(plane)) + _verts3d = tuple(plane.xy_to_xyz(_v) for _v in mesh_2d.vertices) + return cls(_verts3d, mesh_2d.faces, mesh_2d.colors)
+ + +
+[docs] + @classmethod + def from_stl(cls, file_path): + """Create a Mesh3D from an STL file. + + Args: + file_path: Path to an STL file as a text string. The STL file can be + in either ASCII or binary format. + """ + from ladybug_geometry.interop.stl import STL # avoid circular import + face_vertices = STL.from_file(file_path).face_vertices + return cls.from_face_vertices(face_vertices)
+ + +
+[docs] + @classmethod + def from_obj(cls, file_path): + """Create a Mesh3D from an OBJ file. + + Args: + file_path: Path to an OBJ file as a text string. + """ + from ladybug_geometry.interop.obj import OBJ # avoid circular import + transl_obj = OBJ.from_file(file_path) + return cls(transl_obj.vertices, transl_obj.faces, transl_obj.vertex_colors)
+ + + @property + def min(self): + """A Point3D for the minimum bounding box vertex around this mesh.""" + if self._min is None: + self._calculate_min_max() + return self._min + + @property + def max(self): + """A Point3D for the maximum bounding box vertex around this mesh.""" + if self._max is None: + self._calculate_min_max() + return self._max + + @property + def center(self): + """A Point3D for the center of the bounding box around this mesh.""" + if self._center is None: + min, max = self.min, self.max + self._center = Point3D( + (min.x + max.x) / 2, (min.y + max.y) / 2, (min.z + max.z) / 2) + return self._center + + @property + def face_areas(self): + """A tuple of face areas that parallels the faces property.""" + if self._face_normals is None: + self._calculate_face_areas_and_normals() + elif isinstance(self._face_areas, (float, int)): # same area for each face + self._face_areas = tuple(self._face_areas for face in self.faces) + return self._face_areas + + @property + def face_normals(self): + """Tuple of Vector3D objects for all face normals.""" + if self._face_normals is None: + self._calculate_face_areas_and_normals() + elif isinstance(self._face_normals, Vector3D): # same normal for each face + self._face_normals = tuple(self._face_normals for face in self.faces) + return self._face_normals + + @property + def vertex_normals(self): + """Tuple of Vector3D objects for all vertex normals.""" + if not self._vertex_normals: + self._calculate_vertex_normals() + elif isinstance(self._vertex_normals, Vector3D): # same normal for each vertex + self._vertex_normals = tuple(self._vertex_normals for face in self.vertices) + return self._vertex_normals + + @property + def face_edges(self): + """List of polylines with one Polyline3D for each face. + + This is faster to compute compared to the edges and results in effectively + the same type of wireframe visualization. + """ + _all_verts = self._vertices + f_edges = [] + for face in self._faces: + verts = tuple(_all_verts[v] for v in face) + (_all_verts[face[0]],) + f_edges.append(Polyline3D(verts)) + return f_edges + + @property + def edges(self): + """"Tuple of all edges in this Mesh3D as LineSegment3D objects. + + Note that this method will return only the unique edges in the mesh without + any duplicates. This is sometimes desirable but can take a lot of time + to compute for large meshes. For a faster property, use face_edges.""" + if self._edges is None: + if self._edge_indices is None: + self._compute_edge_info() + self._edges = tuple(LineSegment3D.from_end_points( + self.vertices[seg[0]], self.vertices[seg[1]]) + for seg in self._edge_indices) + return self._edges + + @property + def naked_edges(self): + """"Tuple of all naked edges in this Mesh3D as LineSegment3D objects. + + Naked edges belong to only one face in the mesh (they are not + shared between faces). + """ + if self._naked_edges is None: + self._naked_edges = self._get_edge_type(0) + return self._naked_edges + + @property + def internal_edges(self): + """"Tuple of all internal edges in this Mesh3D as LineSegment3D objects. + + Internal edges are shared between two faces in the mesh. + """ + if self._internal_edges is None: + self._internal_edges = self._get_edge_type(1) + return self._internal_edges + + @property + def non_manifold_edges(self): + """"Tuple of all non-manifold edges in this mesh as LineSegment3D objects. + + Non-manifold edges are shared between three or more faces. + """ + if self._non_manifold_edges is None: + if self._edges is None: + self.edges + nm_edges = [] + for i, type in enumerate(self._edge_types): + if type > 1: + nm_edges.append(self._edges[i]) + self._non_manifold_edges = tuple(nm_edges) + return self._non_manifold_edges + +
+[docs] + def remove_vertices(self, pattern): + """Get a version of this mesh where vertices are removed according to a pattern. + + Args: + pattern: A list of boolean values denoting whether a vertex should + remain in the mesh (True) or be removed from the mesh (False). + The length of this list must match the number of this mesh's vertices. + + Returns: + A tuple with two elements. + + - new_mesh: + A mesh where the vertices have been removed according + to the input pattern. + + - face_pattern: + A list of boolean values that corresponds to the + original mesh faces noting whether the face is in the new mesh + (True) or has been removed from the new mesh (False). + """ + _new_verts, _new_faces, _new_colors, _new_f_cent, _new_f_area, face_pattern = \ + self._remove_vertices(pattern) + + new_mesh = Mesh3D(_new_verts, _new_faces, _new_colors) + new_mesh._face_centroids = _new_f_cent + new_mesh._face_areas = _new_f_area + return new_mesh, face_pattern
+ + +
+[docs] + def remove_faces(self, pattern): + """Get a version of this mesh where faces are removed according to a pattern. + + Args: + pattern: A list of boolean values denoting whether a face should + remain in the mesh (True) or be removed from the mesh (False). + The length of this list must match the number of this mesh's faces. + + Returns: + A tuple with two elements. + + - new_mesh: + A mesh where the faces have been removed according + to the input pattern. + + - vertex_pattern: + A list of boolean values that corresponds to the + original mesh vertices noting whether the vertex is in the new mesh + (True) or has been removed from the new mesh (False). + """ + vertex_pattern = self._vertex_pattern_from_remove_faces(pattern) + _new_verts, _new_faces, _new_colors, _new_f_cent, _new_f_area, face_pattern = \ + self._remove_vertices(vertex_pattern, pattern) + + new_mesh = Mesh3D(_new_verts, _new_faces, _new_colors) + new_mesh._face_centroids = _new_f_cent + new_mesh._face_areas = _new_f_area + return new_mesh, vertex_pattern
+ + +
+[docs] + def remove_faces_only(self, pattern): + """Get a version of this mesh where faces are removed and vertices are unaltered. + + This is faster than the Mesh3D.remove_faces method but will likely result + a lower-quality mesh where several vertices exist in the mesh that are not + referenced by any face. This may be preferred if pure speed of removing + faces is a priority over smallest size of the mesh in memory. + + Args: + pattern: A list of boolean values denoting whether a face should + remain in the mesh (True) or be removed from the mesh (False). + The length of this list must match the number of this mesh's faces. + + Returns: + new_mesh -- A mesh where the faces have been removed according + to the input pattern. + """ + _new_faces, _new_colors, _new_f_cent, _new_f_area = \ + self._remove_faces_only(pattern) + + new_mesh = Mesh3D(self.vertices, _new_faces, _new_colors) + new_mesh._face_centroids = _new_f_cent + new_mesh._face_areas = _new_f_area + return new_mesh
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate a mesh by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the point will be rotated. + """ + _verts = tuple(pt.rotate(axis, angle, origin) for pt in self.vertices) + return self._mesh_transform(_verts)
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a mesh rotated counterclockwise in the XY plane by a certain angle. + + Args: + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the point will be rotated. + """ + _verts = tuple(pt.rotate_xy(angle, origin) for pt in self.vertices) + return self._mesh_transform(_verts)
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a mesh by a factor from an origin point. + + Args: + factor: A number representing how much the mesh should be scaled. + origin: A Point representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + if origin is None: + _verts = tuple( + Point3D(pt.x * factor, pt.y * factor, pt.z * factor) + for pt in self.vertices) + else: + _verts = tuple(pt.scale(factor, origin) for pt in self.vertices) + return self._mesh_scale(_verts, factor)
+ + +
+[docs] + def offset_mesh(self, distance): + """Get a Mesh3D that has been offset from this one by a certain difference. + + Effectively, this method moves each mesh vertex along the vertex normal + by the offset distance. + + Args: + distance: A number for the distance to offset the mesh. + """ + new_verts = tuple(pt.move(norm * distance) for pt, norm in + zip(self.vertices, self.vertex_normals)) + return Mesh3D(new_verts, self.faces, self._colors)
+ + +
+[docs] + def height_field_mesh(self, values, domain): + """Get a Mesh3D that has faces or vertices offset according to a list of values. + + Args: + values: A list of values that has a length matching the number of faces + or vertices in this mesh. + domain: A tuple or list of two numbers for the upper and lower distances + that the mesh vertices should be offset. (ie. (0, 3)) + """ + assert isinstance(domain, (tuple, list)), 'Expected tuple for domain. '\ + 'Got {}.'.format(type(domain)) + assert len(domain) == 2, 'Expected domain to be in the format (min, max). ' \ + 'Got {}.'.format(domain) + + if len(values) == len(self.faces): + remap_vals = Mesh3D._remap_values(values, domain[0], domain[-1]) + vert_remap_vals = [] + for vf in self.vertex_connected_faces: + v = 0 + for j in vf: + v += remap_vals[j] + try: + v /= len(vf) # average the vertex value over its connected faces + except ZeroDivisionError: + pass # lone vertex without any faces + vert_remap_vals.append(v) + new_verts = tuple(pt.move(norm * dist) for pt, norm, dist in + zip(self.vertices, self.vertex_normals, vert_remap_vals)) + elif len(values) == len(self.vertices): + remap_vals = Mesh3D._remap_values(values, domain[0], domain[-1]) + new_verts = tuple(pt.move(norm * dist) for pt, norm, dist in + zip(self.vertices, self.vertex_normals, remap_vals)) + else: + raise ValueError( + 'Input values for height_field_mesh ({}) does not match the number of' + ' mesh faces ({}) nor the number of vertices ({}).' + .format(len(values), len(self.faces), len(self.vertices))) + return Mesh3D(new_verts, self.faces, self._colors)
+ + +
+[docs] + def to_dict(self): + """Get Mesh3D as a dictionary.""" + base = {'type': 'Mesh3D', + 'vertices': [pt.to_array() for pt in self.vertices], + 'faces': self.faces} + if self.colors is not None: + base['colors'] = [col.to_dict() for col in self.colors] + return base
+ + +
+[docs] + def to_stl(self, folder, name=None): + """Write the Mesh3D to an ASCII STL file. + + Args: + folder: A text string for the directory where the STL will be written. + name: A text string for the name of the STL file. + """ + from ladybug_geometry.interop.stl import STL # avoid circular import + stl_obj = STL(self.face_vertices, self.face_normals) + return stl_obj.to_file(folder, name)
+ + +
+[docs] + def to_obj(self, folder, name, include_colors=True, include_normals=False, + triangulate_quads=False, include_mtl=False): + """Write the Mesh3D to an ASCII OBJ file. + + Args: + folder: A text string for the directory where the OBJ will be written. + name: A text string for the name of the OBJ file. + include_colors: Boolean to note whether the Mesh3D colors should be + included in the OBJ file. (Default: True). + include_normals: Boolean to note whether the vertex normals should be + included in the OBJ file. (Default: False). + triangulate_quads: Boolean to note whether quad faces should be + triangulated upon export to OBJ. This may be needed for certain + software platforms that require the mesh to be composed entirely + of triangles (eg. Radiance). (Default: False). + include_mtl: Boolean to note whether an .mtl file should be automatically + generated next to the .obj file in the output folder. All materials + in the mtl file will be diffuse white, with the assumption that + these will be customized later. (Default: False). + """ + from ladybug_geometry.interop.obj import OBJ # avoid circular import + transl_obj = OBJ.from_mesh3d(self, include_colors, include_normals) + return transl_obj.to_file(folder, name, triangulate_quads, include_mtl)
+ + +
+[docs] + @staticmethod + def join_meshes(meshes): + """Join an array of Mesh3Ds into a single Mesh3D. + + Args: + meshes: An array of meshes to be joined into one. + + Returns: + A single Mesh3D object derived from the input meshes. + """ + # set up empty lists of objects to be filled + verts = [] + faces = [] + colors = [] + + # loop through all of the meshes and get new faces + total_v_i = 0 + for mesh in meshes: + verts.extend(mesh._vertices) + for fc in mesh._faces: + faces.append(tuple(v_i + total_v_i for v_i in fc)) + total_v_i += len(mesh._vertices) + if mesh._colors: + colors.extend(mesh._colors) + + # create the new mesh + if len(colors) != 0: + new_mesh = Mesh3D(verts, faces, colors) + else: + new_mesh = Mesh3D(verts, faces) + return new_mesh
+ + + def _calculate_min_max(self): + """Calculate maximum and minimum Point3D for this object.""" + min_pt = [self.vertices[0].x, self.vertices[0].y, self.vertices[0].z] + max_pt = [self.vertices[0].x, self.vertices[0].y, self.vertices[0].z] + + for v in self.vertices[1:]: + if v.x < min_pt[0]: + min_pt[0] = v.x + elif v.x > max_pt[0]: + max_pt[0] = v.x + if v.y < min_pt[1]: + min_pt[1] = v.y + elif v.y > max_pt[1]: + max_pt[1] = v.y + if v.z < min_pt[2]: + min_pt[2] = v.z + elif v.z > max_pt[2]: + max_pt[2] = v.z + + self._min = Point3D(min_pt[0], min_pt[1], min_pt[2]) + self._max = Point3D(max_pt[0], max_pt[1], max_pt[2]) + + def _calculate_face_areas_and_normals(self): + """Calculate face areas and normals from vertices.""" + _f_norm = [] + _f_area = [] + for face in self.faces: + pts = tuple(self._vertices[i] for i in face) + if len(face) == 3: + n, a = self._calculate_normal_and_area_for_triangle(pts) + else: + n, a = self._calculate_normal_and_area_for_quad(pts) + _f_norm.append(n) + _f_area.append(a) + self._face_normals = tuple(_f_norm) + self._face_areas = tuple(_f_area) + + def _calculate_vertex_normals(self): + """Calculate vertex normals. + + This is accomplished by normalizing the average of the surface normals + of the faces that contain that vertex. This particular method weights + this average by the area of each face, though this does not always need + to be the case as noted here: + https://en.wikipedia.org/wiki/Vertex_normal + """ + # find shared faces for each vertices + mapper = [[] for v in xrange(len(self.vertices))] + for c, face in enumerate(self.faces): + for i in face: + mapper[i].append(c) + # now calculate vertex normal based on face normals + vn = [] + fn = self.face_normals + fa = self.face_areas + for fi in mapper: + x, y, z = 0, 0, 0 + for n, a in zip(tuple(fn[i] for i in fi), tuple(fa[i] for i in fi)): + x += n.x * a + y += n.y * a + z += n.z * a + _v = Vector3D(x, y, z) + vn.append(_v.normalize()) + self._vertex_normals = tuple(vn) + + def _get_edge_type(self, edge_type): + """Get all of the edges of a certain type in this mesh.""" + if self._edges is None: + self.edges + sel_edges = [] + for i, type in enumerate(self._edge_types): + if type == edge_type: + sel_edges.append(self._edges[i]) + return tuple(sel_edges) + + def _tri_face_centroid(self, face): + """Compute the centroid of a triangular face.""" + return Mesh3D._tri_centroid(tuple(self._vertices[i] for i in face)) + + def _quad_face_centroid(self, face): + """Compute the centroid of a quadrilateral face.""" + return Mesh3D._quad_centroid(tuple(self._vertices[i] for i in face)) + + def _mesh_transform(self, verts): + """Transform mesh in a way that transfers properties and avoids extra checks.""" + _new_mesh = Mesh3D(verts, self.faces) + self._transfer_properties(_new_mesh) + return _new_mesh + + def _mesh_transform_move(self, verts): + """Move mesh in a way that transfers properties and avoids extra checks.""" + _new_mesh = Mesh3D(verts, self.faces) + self._transfer_properties(_new_mesh) + _new_mesh._face_normals = self._face_normals + _new_mesh._vertex_normals = self._vertex_normals + return _new_mesh + + def _mesh_scale(self, verts, factor): + """Scale mesh in a way that transfers properties and avoids extra checks.""" + _new_mesh = Mesh3D(verts, self.faces) + self._transfer_properties_scale(_new_mesh, factor) + _new_mesh._face_normals = self._face_normals + _new_mesh._vertex_normals = self._vertex_normals + return _new_mesh + + def _check_vertices_input(self, vertices): + """Check the input vertices.""" + if not isinstance(vertices, tuple): + vertices = tuple(vertices) + for vert in vertices: + assert isinstance(vert, Point3D), \ + 'Expected Point3D for {} vertex. Got {}.'.format( + self.__class__.__name__, type(vert)) + return vertices + + @staticmethod + def _calculate_normal_and_area_for_triangle(pts): + """Calculate normal and area for three points. + + Returns: + n = Normalized normal vector for the triangle. + a = Area of the triangle. + """ + v1 = pts[1] - pts[0] + v2 = pts[2] - pts[0] + n = v1.cross(v2) + a = n.magnitude / 2 + return n.normalize(), a + + @staticmethod + def _calculate_normal_and_area_for_quad(pts): + """Calculate normal and area for four points. + + This method uses an area-weighted average of the two triangle normals + that compose the quad face. + + Returns: + n = Normalized normal vector for the quad. + a = Area of the quad. + """ + # TODO: Get this method to work for concave quads. + # This method is only reliable when quads are convex since we assume + # either diagonal of the quad splits it into two triangles. + # It seems Rhino never produces concave quads when it automatically meshes + # but we will likely want to add support for this if meshes have other origins + v1 = pts[1] - pts[0] + v2 = pts[2] - pts[0] + n1 = v1.cross(v2) + + v3 = pts[3] - pts[2] + v4 = pts[1] - pts[2] + n2 = v3.cross(v4) + + a = (n1.magnitude + n2.magnitude) / 2 + n = Vector3D((n1.x + n2.x) / 2, (n1.y + n2.y) / 2, (n1.z + n2.z) / 2) + return n.normalize(), a + + @staticmethod + def _face_center(verts): + """Get the center of a list of Point3D vertices.""" + _cent_x = sum([v.x for v in verts]) + _cent_y = sum([v.y for v in verts]) + _cent_z = sum([v.z for v in verts]) + v_count = len(verts) + return Point3D(_cent_x / v_count, _cent_y / v_count, _cent_z / v_count) + + @staticmethod + def _tri_centroid(verts): + """Get the centroid of a list of 3 Point3D vertices.""" + _cent_x = sum([v.x for v in verts]) + _cent_y = sum([v.y for v in verts]) + _cent_z = sum([v.z for v in verts]) + return Point3D(_cent_x / 3, _cent_y / 3, _cent_z / 3) + + @staticmethod + def _quad_centroid(verts): + """Get the centroid of a list of 4 Point3D vertices.""" + # TODO: Get this method to recognize concave quads. + # This method is only reliable when quads are convex since we assume + # either diagonal of the quad splits it into two triangles. + # It seems Rhino never produces concave quads when it automatically meshes + _tri_verts = ((verts[0], verts[1], verts[2]), (verts[2], verts[3], verts[0])) + _tri_c = [Mesh3D._tri_centroid(tri) for tri in _tri_verts] + _tri_a = [Mesh3D._get_tri_area(tri) for tri in _tri_verts] + _tot_a = sum(_tri_a) + try: + _cent_x = (_tri_c[0].x * _tri_a[0] + _tri_c[1].x * _tri_a[1]) / _tot_a + _cent_y = (_tri_c[0].y * _tri_a[0] + _tri_c[1].y * _tri_a[1]) / _tot_a + _cent_z = (_tri_c[0].z * _tri_a[0] + _tri_c[1].z * _tri_a[1]) / _tot_a + except ZeroDivisionError: + _cent_x = sum([v.x for v in verts]) / 4 + _cent_y = sum([v.y for v in verts]) / 4 + _cent_z = sum([v.z for v in verts]) / 4 + return Point3D(_cent_x, _cent_y, _cent_z) + + @staticmethod + def _get_tri_area(pts): + """Get the area of a triangle from three Point3D objects.""" + v1 = pts[1] - pts[0] + v2 = pts[2] - pts[0] + n1 = v1.cross(v2) + return n1.magnitude / 2 + + @staticmethod + def _remap_values(values, tmin, tmax): + """Remap a set of values to offset distances within a domain.""" + omin = min(values) + omax = max(values) + odiff = omax - omin + tdiff = tmax - tmin + if odiff == 0: + return [tmin] * len(values) + else: + return [(v - omin) * tdiff / odiff + tmin for v in values] + + def __copy__(self): + _new_mesh = Mesh3D(self.vertices, self.faces) + self._transfer_properties(_new_mesh) + _new_mesh._face_centroids = self._face_centroids + _new_mesh._face_normals = self._face_normals + _new_mesh._vertex_normals = self._vertex_normals + return _new_mesh + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return tuple(hash(pt) for pt in self._vertices) + \ + tuple(hash(face) for face in self._faces) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Mesh3D) and self.__key() == other.__key() + + def __repr__(self): + return 'Mesh3D ({} faces) ({} vertices)'.format( + len(self.faces), len(self))
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/plane.html b/docs/_modules/ladybug_geometry/geometry3d/plane.html new file mode 100644 index 00000000..e3986e62 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/plane.html @@ -0,0 +1,1695 @@ + + + + + + + ladybug_geometry.geometry3d.plane — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.plane

+# coding=utf-8
+"""Plane"""
+from __future__ import division
+
+from .pointvector import Point3D, Vector3D
+from .ray import Ray3D
+from ..intersection3d import intersect_line3d_plane, intersect_line3d_plane_infinite, \
+    intersect_plane_plane, closest_point3d_on_plane, \
+    closest_point3d_between_line3d_plane
+from ..geometry2d.pointvector import Point2D, Vector2D
+from ..geometry2d.ray import Ray2D
+
+import math
+
+
+
+[docs] +class Plane(object): + """Plane object. + + Args: + n: A Vector3D representing the normal of the plane. + o: A Point3D representing the origin point of the plane. + x: An optional Vector3D for the X-Axis of the Plane. + Note that this vector must be orthogonal to the input normal vector. + If None, the default will find an X-Axis in the world XY plane. + + Properties: + * n + * o + * k + * x + * y + * tilt + * altitude + * azimuth + * min + * max + """ + __slots__ = ('_n', '_o', '_k', '_x', '_y', '_altitude', '_azimuth') + + def __init__(self, n=Vector3D(0, 0, 1), o=Point3D(0, 0, 0), x=None): + """Initialize Plane.""" + assert isinstance(n, Vector3D), \ + "Expected Vector3D for plane normal. Got {}.".format(type(n)) + assert isinstance(o, Point3D), \ + "Expected Point3D for plane origin. Got {}.".format(type(o)) + self._n = n.normalize() + self._o = o + self._k = self._n.dot(self._o) + + if x is None: + if self._n.x == 0 and self._n.y == 0: + self._x = Vector3D(1, 0, 0) + else: + x = Vector3D(self._n.y, -self._n.x, 0) + x = x.normalize() + self._x = x + else: + assert isinstance(x, Vector3D), \ + "Expected Vector3D for plane X-axis. Got {}.".format(type(x)) + x = x.normalize() + assert abs(self._n.x * x.x + self._n.y * x.y + self._n.z * x.z) < 1e-2, \ + 'Plane X-axis and normal vector are not orthogonal. Got angle of {} ' \ + 'degrees between them.'.format(math.degrees(self._n.angle(x))) + self._x = x + self._y = self._n.cross(self._x) + self._altitude = None + self._azimuth = None + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Plane from a dictionary. + + .. code-block:: python + + { + "type": "Plane" + "n": (0, 0, 1), + "o": (0, 10, 0), + "x": (1, 0, 0) + } + """ + x = None + if 'x' in data and data['x'] is not None: + x = Vector3D.from_array(data['x']) + return cls(Vector3D.from_array(data['n']), + Point3D.from_array(data['o']), x)
+ + +
+[docs] + @classmethod + def from_three_points(cls, o, p2, p3): + """Initialize a Plane from three Point3D objects that are not co-linear. + + Args: + o: A Point3D representing the origin point of the plane. + p2: A Point3D representing a point the plane. + p3: A Point3D representing a point the plane. + """ + return cls((p2 - o).cross(p3 - o), o)
+ + +
+[docs] + @classmethod + def from_normal_k(cls, n, k): + """Initialize a Plane from a normal vector and a scalar constant. + + Args: + o: A Point3D representing the origin point of the plane. + k: Scalar constant relating origin point to normal vector + """ + # get an arbitrary point on the plane for the origin + if n.z: + o = Point3D(0., 0., k / n.z) + elif n.y: + o = Point3D(0., k / n.y, 0.) + else: + o = Point3D(k / n.x, 0., 0.) + return cls(n, o)
+ + + @property + def n(self): + """Normal vector. This vector will always be normalized (magnitude = 1).""" + return self._n + + @property + def o(self): + """Origin point.""" + return self._o + + @property + def k(self): + """Scalar constant relating origin point to normal vector.""" + return self._k + + @property + def x(self): + """Plane X-Axis. This vector will always be normalized (magnitude = 1).""" + return self._x + + @property + def y(self): + """Plane Y-Axis. This vector will always be normalized (magnitude = 1).""" + return self._y + + @property + def azimuth(self): + """Get the azimuth of the plane. + + This is always between 0, indicating the positive Y-axis, and moving clockwise + up to 2 * Pi, which indicates a return to the positive Y-axis. + + This will be zero if the plane is perfectly horizontal. + """ + if self._azimuth is None: + try: + n_vec = Vector2D(0, 1) + self._azimuth = n_vec.angle_clockwise(Vector2D(self.n.x, self.n.y)) + except ZeroDivisionError: # plane is perfectly horizontal + self._azimuth = 0 + return self._azimuth + + @property + def altitude(self): + """Get the altitude of the plane. Between Pi/2 (up) and -Pi/2 (down).""" + if self._altitude is None: + self._altitude = self.n.angle(Vector3D(0, 0, -1)) - math.pi / 2 + return self._altitude + + @property + def tilt(self): + """Get the tilt of the plane. Between 0 (up) and Pi (down).""" + return abs(self.altitude - (math.pi / 2)) + + @property + def min(self): + """Returns the Plane origin.""" + return self._o + + @property + def max(self): + """Returns the Plane origin.""" + return self._o + +
+[docs] + def flip(self): + """Get a flipped version of this plane (facing the opposite direction).""" + return Plane(self.n.reverse(), self.o, self.x)
+ + +
+[docs] + def move(self, moving_vec): + """Get a plane that has been moved along a vector. + + Args: + moving_vec: A Vector3D with the direction and distance to move the plane. + """ + return Plane(self.n, self.o.move(moving_vec), self.x)
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate a plane by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + return Plane(self.n.rotate(axis, angle), + self.o.rotate(axis, angle, origin), + self.x.rotate(axis, angle))
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a plane rotated counterclockwise in the world XY plane by a certain angle. + + Args: + angle: An angle in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + return Plane(self.n.rotate_xy(angle), + self.o.rotate_xy(angle, origin), + self.x.rotate_xy(angle))
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a plane reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the plane will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin from which to reflect. + """ + return Plane(self.n.reflect(normal), + self.o.reflect(normal, origin), + self.x.reflect(normal))
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a plane by a factor from an origin point. + + Args: + factor: A number representing how much the plane should be scaled. + origin: A Point3D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + return Plane(self.n, self.o.scale(factor, origin), self.x)
+ + +
+[docs] + def xyz_to_xy(self, point): + """Get a Point2D in the coordinate system of this plane from a Point3D. + + Note that the input Point3D should lie within this plane object in order + for the result to be valid. + """ + _diff = Vector3D(point.x - self.o.x, point.y - self.o.y, point.z - self.o.z) + return Point2D(self.x.dot(_diff), self.y.dot(_diff))
+ + +
+[docs] + def xy_to_xyz(self, point): + """Get a Point3D from a Point2D in the coordinate system of this plane.""" + # This method returns the same result as the following code: + # self.o + (self.x * point.x) + (self.y * point.y) + # It has been written explicitly to cut out the isinstance() checks for speed + _u = (self.x.x * point.x, self.x.y * point.x, self.x.z * point.x) + _v = (self.y.x * point.y, self.y.y * point.y, self.y.z * point.y) + return Point3D( + self.o.x + _u[0] + _v[0], self.o.y + _u[1] + _v[1], self.o.z + _u[2] + _v[2])
+ + +
+[docs] + def is_point_above(self, point): + """Test if a given point is above or below this plane. + + Above is defined as being on the side of the plane that the plane normal + is pointing towards. + + Args: + point: A Point3D object to test. + + Returns: + True is point is above; False if below. + """ + vec = Vector3D(point.x - self.o.x, point.y - self.o.y, point.z - self.o.z) + return self.n.dot(vec) > 0
+ + +
+[docs] + def closest_point(self, point): + """Get the closest Point3D on this plane to another Point3D. + + Args: + point: A Point3D object to which the closest point on this plane + will be computed. + + Returns: + Point3D for the closest point on this plane to the input point. + """ + return closest_point3d_on_plane(point, self)
+ + +
+[docs] + def distance_to_point(self, point): + """Get the minimum distance between this plane and the input point. + + Args: + point: A Point3D object to which the minimum distance will be computed. + + Returns: + The distance to the input point. + """ + close_pt = self.closest_point(point) + return point.distance_to_point(close_pt)
+ + +
+[docs] + def closest_points_between_line(self, line_ray): + """Get the two closest Point3D between this plane and a Line3D or Ray3D. + + Args: + line_ray: A Line3D or Ray3D object to which the closest points + will be computed. + + Returns: + Two Point3D objects representing + + 1) The closest point on the input line_ray to this plane. + 2) The closest point on this plane to the input line_ray. + + Will be None if the line_ray intersects this plant + """ + return closest_point3d_between_line3d_plane(line_ray, self)
+ + +
+[docs] + def distance_to_line(self, line_ray): + """Get the minimum distance between this plane and the input Line3D or Ray3D. + + Args: + line_ray: A Line3D or Ray3D object to which the minimum distance + will be computed. + + Returns: + The minimum distance to the input line_ray. + """ + result = self.closest_points_between_line(line_ray) + if result is None: # intersection + return 0 + else: + return result[0].distance_to_point(result[1])
+ + +
+[docs] + def project_point(self, point, projection_direction=None): + """Project a point onto this Plane given a certain projection direction. + + Args: + point: A Point3D to be projected onto the plane + projection_direction: A Line3D or Ray3D object to set the direction + of projection. If None, this Plane's normal will be + used. (Default: None). + + Returns: + Point3D for the projected point. Will be None if the projection_direction + is parallel to the plane. + """ + int_ray = Ray3D(point, self.n) if projection_direction is None \ + else Ray3D(point, projection_direction) + return intersect_line3d_plane_infinite(int_ray, self)
+ + +
+[docs] + def intersect_line_ray(self, line_ray): + """Get the intersection between this plane and the input Line3D or Ray3D. + + Args: + line_ray: A Line3D or Ray3D object for which intersection will be computed. + + Returns: + Point3D for the intersection. Will be None if no intersection exists. + """ + return intersect_line3d_plane(line_ray, self)
+ + +
+[docs] + def intersect_arc(self, arc): + """Get the intersection between this Plane and an Arc3D. + + Args: + plane: A Plane object for which intersection will be computed. + + Returns: + A list of 2 Point3D objects if a full intersection exists. + A list with a single Point3D object if the line is tangent or intersects + only once. None if no intersection exists. + """ + _plane_int_ray = self.intersect_plane(arc.plane) + if _plane_int_ray is not None: + _p12d = arc.plane.xyz_to_xy(_plane_int_ray.p) + _p22d = arc.plane.xyz_to_xy(_plane_int_ray.p + _plane_int_ray.v) + _v2d = _p22d - _p12d + _int_ray2d = Ray2D(_p12d, _v2d) + _int_pt2d = arc.arc2d.intersect_line_infinite(_int_ray2d) + if _int_pt2d is not None: + return [arc.plane.xy_to_xyz(pt) for pt in _int_pt2d] + return None
+ + +
+[docs] + def intersect_plane(self, plane): + """Get the intersection between this Plane and another Plane. + + Args: + plane: A Plane object for which intersection will be computed. + + Returns: + Ray3D for the intersection. Will be None if planes are parallel. + """ + result = intersect_plane_plane(self, plane) + if result is not None: + return Ray3D(result[0], result[1]) + return None
+ + +
+[docs] + def is_coplanar(self, plane): + """Test if another Plane object is perfectly coplanar with this Plane. + + Args: + plane: A Plane object for which co-planarity will be tested. + + Returns: + True if plane is coplanar. False if it is not coplanar. + """ + if self.n == plane.n: + return self.k == plane.k + elif self.n == plane.n.reverse(): + return self.k == -plane.k + return False
+ + +
+[docs] + def is_coplanar_tolerance(self, plane, tolerance, angle_tolerance): + """Test if another Plane object is coplanar within a certain tolerance. + + Args: + plane: A Plane object for which co-planarity will be tested. + tolerance: The distance between the two planes at which point they can + be considered coplanar. + angle_tolerance: The angle in radians that the plane normals can + differ from one another in order for the planes to be considered + coplanar. + + Returns: + True if plane is coplanar. False if it is not coplanar. + """ + if self.n.angle(plane.n) <= angle_tolerance or \ + self.n.angle(plane.n.reverse()) <= angle_tolerance: + return self.distance_to_point(plane.o) <= tolerance + return False
+ + +
+[docs] + def duplicate(self): + """Get a copy of this object.""" + return self.__copy__()
+ + +
+[docs] + def to_dict(self): + """Get Plane as a dictionary.""" + return {'type': 'Plane', + 'n': self.n.to_array(), + 'o': self.o.to_array(), + 'x': self.x.to_array()}
+ + + def __copy__(self): + return Plane(self.n, self.o, self.x) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self.n, self.o, self.x) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Plane) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + +
+[docs] + def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__()
+ + + def __repr__(self): + return 'Plane (<%.2f, %.2f, %.2f> normal) (<%.2f, %.2f, %.2f> origin)' % \ + (self.n.x, self.n.y, self.n.z, self.o.x, self.o.y, self.o.z)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/pointvector.html b/docs/_modules/ladybug_geometry/geometry3d/pointvector.html new file mode 100644 index 00000000..76eb5c17 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/pointvector.html @@ -0,0 +1,1779 @@ + + + + + + + ladybug_geometry.geometry3d.pointvector — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.pointvector

+# coding=utf-8
+"""3D Vector and 3D Point"""
+from __future__ import division
+
+from ..geometry2d.pointvector import Vector2D
+
+import math
+import operator
+
+
+
+[docs] +class Vector3D(object): + """3D Vector object. + + Args: + x: Number for the X coordinate. + y: Number for the Y coordinate. + z: Number for the Z coordinate. + + Properties: + * x + * y + * z + * magnitude + * magnitude_squared + * is_zero + """ + __slots__ = ('_x', '_y', '_z') + + def __init__(self, x=0, y=0, z=0): + """Initialize 3D Vector.""" + self._x = self._cast_to_float(x) + self._y = self._cast_to_float(y) + self._z = self._cast_to_float(z) + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Vector3D/Point3D from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "x": 10, + "y": 0, + "z": 0 + } + """ + return cls(data['x'], data['y'], data['z'])
+ + +
+[docs] + @classmethod + def from_array(cls, array): + """Initialize a Vector3D/Point3D from an array. + + Args: + array: A tuple or list with three numbers representing the x, y and z + values of the point. + """ + return cls(array[0], array[1], array[2])
+ + +
+[docs] + @classmethod + def from_vector2d(cls, vector2d, z=0): + """Initialize a new Vector3D from an Vector2D and a z value. + + Args: + line2d: A Vector2D to be used to generate the Vector3D. + z: A number for the Z coordinate value of the line. + """ + return cls(vector2d.x, vector2d.y, z)
+ + + @property + def x(self): + """Get the X coordinate.""" + return self._x + + @property + def y(self): + """Get the Y coordinate.""" + return self._y + + @property + def z(self): + """Get the Z coordinate.""" + return self._z + + @property + def magnitude(self): + """Get the magnitude of the vector.""" + return self.__abs__() + + @property + def magnitude_squared(self): + """Get the magnitude squared of the vector.""" + return self.x ** 2 + self.y ** 2 + self.z ** 2 + + @property + def min(self): + """Always equal to (0, 0, 0). + + This property exists to help with bounding box calculations. + """ + return Point3D(0, 0, 0) + + @property + def max(self): + """Always equal to (0, 0, 0). + + This property exists to help with bounding box calculations. + """ + return Point3D(0, 0, 0) + +
+[docs] + def is_zero(self, tolerance): + """Boolean to note whether the vector is within a given zero tolerance. + + Args: + tolerance: The tolerance below which the vector is considered to + be a zero vector. + """ + return abs(self.x) <= tolerance and abs(self.y) <= tolerance and \ + abs(self.z) <= tolerance
+ + +
+[docs] + def is_equivalent(self, other, tolerance): + """Test whether this object is equivalent to another within a certain tolerance. + + Note that if you want to test whether the coordinate values are perfectly + equal to one another, the == operator can be used. + + Args: + other: Another Point3D or Vector3D for which geometric equivalency + will be tested. + tolerance: The minimum difference between the coordinate values of two + objects at which they can be considered geometrically equivalent. + + Returns: + True if equivalent. False if not equivalent. + """ + return abs(self.x - other.x) <= tolerance and \ + abs(self.y - other.y) <= tolerance and \ + abs(self.z - other.z) <= tolerance
+ + +
+[docs] + def normalize(self): + """Get a copy of the vector that is a unit vector (magnitude=1).""" + d = self.magnitude + try: + return Vector3D(self.x / d, self.y / d, self.z / d) + except ZeroDivisionError: + return self.duplicate()
+ + +
+[docs] + def reverse(self): + """Get a copy of this vector that is reversed.""" + return self.__neg__()
+ + +
+[docs] + def dot(self, other): + """Get the dot product of this vector with another.""" + return self.x * other.x + self.y * other.y + self.z * other.z
+ + +
+[docs] + def cross(self, other): + """Get the cross product of this vector and another vector.""" + return Vector3D(self.y * other.z - self.z * other.y, + -self.x * other.z + self.z * other.x, + self.x * other.y - self.y * other.x)
+ + +
+[docs] + def angle(self, other): + """Get the smallest angle between this vector and another.""" + try: + return math.acos(self.dot(other) / (self.magnitude * other.magnitude)) + except ValueError: # python floating tolerance can cause math domain error + if self.dot(other) < 0: + return math.acos(-1) + return math.acos(1)
+ + +
+[docs] + def rotate(self, axis, angle): + """Get a vector rotated around an axis through an angle. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle in radians. + """ + return Vector3D._rotate(self, axis, angle)
+ + +
+[docs] + def rotate_xy(self, angle): + """Get a vector rotated counterclockwise in the XY plane by a certain angle. + + Args: + angle: An angle in radians. + """ + vec_2 = Vector2D._rotate(self, angle) + return Vector3D(vec_2.x, vec_2.y, self.z)
+ + +
+[docs] + def reflect(self, normal): + """Get a vector that is reflected across a plane with the input normal vector. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the vector will be reflected. THIS VECTOR MUST BE NORMALIZED. + """ + return Vector3D._reflect(self, normal)
+ + +
+[docs] + def project(self, normal): + """Get a vector projected into a plane with a given normal. + + Args: + normal: A Vector3D representing the normal vector of the plane into which + the plane will be projected. THIS VECTOR MUST BE NORMALIZED. + """ + return self - normal * self.dot(normal)
+ + +
+[docs] + def duplicate(self): + """Get a copy of this vector.""" + return self.__copy__()
+ + +
+[docs] + def to_dict(self): + """Get Vector3D as a dictionary.""" + return {'type': 'Vector3D', + 'x': self.x, + 'y': self.y, + 'z': self.z}
+ + +
+[docs] + def to_array(self): + """Get Vector3D/Point3D as a tuple of three numbers""" + return (self.x, self.y, self.z)
+ + + def _cast_to_float(self, value): + """Ensure that an input coordinate value is a float.""" + try: + number = float(value) + except Exception: + raise TypeError( + 'Coordinates must be numbers. Got {}: {}.'.format(type(value), value)) + return number + + @staticmethod + def _reflect(vec, normal): + """Hidden reflection method used by both Point3D and Vector3D.""" + d = 2 * (vec.x * normal.x + vec.y * normal.y + vec.z * normal.z) + return Vector3D(vec.x - d * normal.x, + vec.y - d * normal.y, + vec.z - d * normal.z) + + @staticmethod + def _rotate(vec, axis, angle): + """Hidden rotation method used by both Point3D and Vector3D.""" + # Adapted from equations published by Glenn Murray. + # http://inside.mines.edu/~gmurray/ArbitraryAxisRotation/ArbitraryAxisRotation.html + x, y, z = vec.x, vec.y, vec.z + u, v, w = axis.x, axis.y, axis.z + + # Extracted common factors for simplicity and efficiency + r2 = u ** 2 + v ** 2 + w ** 2 + r = math.sqrt(r2) + ct = math.cos(angle) + st = math.sin(angle) / r + dt = (u * x + v * y + w * z) * (1 - ct) / r2 + return Vector3D((u * dt + x * ct + (-w * y + v * z) * st), + (v * dt + y * ct + (w * x - u * z) * st), + (w * dt + z * ct + (-v * x + u * y) * st)) + + def __copy__(self): + return self.__class__(self.x, self.y, self.z) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self.x, self.y, self.z) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, (Vector3D, Point3D)) and \ + self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __nonzero__(self): + return self.x != 0 or self.y != 0 or self.z != 0 + + def __len__(self): + return 3 + + def __getitem__(self, key): + return (self.x, self.y, self.z)[key] + + def __iter__(self): + return iter((self.x, self.y, self.z)) + + def __add__(self, other): + # Vector + Point -> Point + # Vector + Vector -> Vector + if isinstance(other, Point3D): + return Point3D(self.x + other.x, self.y + other.y, self.z + other.z) + elif isinstance(other, Vector3D): + return Vector3D(self.x + other.x, self.y + other.y, self.z + other.z) + else: + raise TypeError('Cannot add {} and {}'.format( + self.__class__.__name__, type(other))) + + __radd__ = __add__ + + def __sub__(self, other): + # Vector - Point -> Point + # Vector - Vector -> Vector + if isinstance(other, Point3D): + return Point3D(self.x - other.x, self.y - other.y, self.z - other.z) + elif isinstance(other, Vector3D): + return Vector3D(self.x - other.x, self.y - other.y, self.z - other.z) + else: + raise TypeError('Cannot subtract {} and {}'.format( + self.__class__.__name__, type(other))) + + def __rsub__(self, other): + if isinstance(other, (Vector3D, Point3D)): + return Vector3D(other.x - self.x, other.y - self.y, other.z - self.z) + else: + assert hasattr(other, '__len__') and len(other) == 3, \ + 'Cannot subtract types {} and {}'.format( + self.__class__.__name__, type(other)) + return Vector3D(other.x - self[0], other.y - self[1], other.z - self[2]) + + def __mul__(self, other): + if isinstance(other, (int, float)): + return Vector3D(self.x * other, self.y * other, self.z * other) + elif isinstance(other, Vector3D): + return Vector3D(self.x * other.x, self.y * other.y, self.z * other.z) + elif isinstance(other, Point3D): + return Point3D(self.x * other.x, self.y * other.y, self.z * other.z) + else: + raise TypeError('Cannot multiply {} and {}'.format( + self.__class__.__name__, type(other))) + + __rmul__ = __mul__ + + def __div__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector3D(self.x / other, self.y / other, self.z / other) + + def __rdiv__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector3D(other / self.x, other / self.y, other / self.z) + + def __floordiv__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector3D(operator.floordiv(self.x, other), + operator.floordiv(self.y, other), + operator.floordiv(self.z, other)) + + def __rfloordiv__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector3D(operator.floordiv(other, self.x), + operator.floordiv(other, self.y), + operator.floordiv(other, self.z)) + + def __truediv__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector3D(operator.truediv(self.x, other), + operator.truediv(self.y, other), + operator.truediv(self.z, other)) + + def __rtruediv__(self, other): + assert type(other) in (int, float), \ + 'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other)) + return Vector3D(operator.truediv(other, self.x), + operator.truediv(other, self.y), + operator.truediv(other, self.z)) + + def __neg__(self): + return Vector3D(-self.x, -self.y, -self.z) + + __pos__ = __copy__ + + def __abs__(self): + return math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2) + +
+[docs] + def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__()
+ + + def __repr__(self): + """Vector3D representation.""" + return 'Vector3D (%.2f, %.2f, %.2f)' % (self.x, self.y, self.z)
+ + + +
+[docs] +class Point3D(Vector3D): + """3D Point object. + + Args: + x: Number for the X coordinate. + y: Number for the Y coordinate. + z: Number for the Z coordinate. + + Properties: + * x + * y + * z + """ + __slots__ = () + +
+[docs] + @classmethod + def from_point2d(cls, point2d, z=0): + """Initialize a new Point3D from an Point2D and a z value. + + Args: + line2d: A Point2D to be used to generate the Point3D. + z: A number for the Z coordinate value of the line. + """ + return cls(point2d.x, point2d.y, z)
+ + + @property + def min(self): + """Always equal to the point itself. + + This property exists to help with bounding box calculations. + """ + return self + + @property + def max(self): + """Always equal to the point itself. + + This property exists to help with bounding box calculations. + """ + return self + +
+[docs] + def move(self, moving_vec): + """Get a point that has been moved along a vector. + + Args: + moving_vec: A Vector3D with the direction and distance to move the point. + """ + return Point3D(self.x + moving_vec.x, + self.y + moving_vec.y, + self.z + moving_vec.z)
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate a point by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the point will be rotated. + """ + return Vector3D._rotate(self - origin, axis, angle) + origin
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a point rotated counterclockwise in the XY plane by a certain angle. + + Args: + angle: An angle in radians. + origin: A Point3D for the origin around which the point will be rotated. + """ + trans_self = self - origin + vec_2 = Vector2D._rotate(trans_self, angle) + return Vector3D(vec_2.x, vec_2.y, trans_self.z) + origin
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a point reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the point will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin from which to reflect. + """ + return Vector3D._reflect(self - origin, normal) + origin
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a point by a factor from an origin point. + + Args: + factor: A number representing how much the point should be scaled. + origin: A Point3D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + if origin is None: + return Point3D(self.x * factor, self.y * factor, self.z * factor) + else: + return (factor * (self - origin)) + origin
+ + +
+[docs] + def project(self, normal, origin): + """Get a point that is projected into a plane with a given normal and origin. + + Args: + normal: A Vector3D representing the normal vector of the plane into which + the plane will be projected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin the plane into which the + point will be projected. + """ + trans_self = self - origin + return self - normal * trans_self.dot(normal)
+ + +
+[docs] + def distance_to_point(self, point): + """Get the distance from this point to another Point3D.""" + vec = (self.x - point.x, self.y - point.y, self.z - point.z) + return math.sqrt(vec[0] ** 2 + vec[1] ** 2 + vec[2] ** 2)
+ + +
+[docs] + def to_dict(self): + """Get Point3D as a dictionary.""" + return {'type': 'Point3D', + 'x': self.x, + 'y': self.y, + 'z': self.z}
+ + + def __add__(self, other): + # Point + Vector -> Point + # Point + Point -> Vector + if isinstance(other, Point3D): + return Vector3D(self.x + other.x, self.y + other.y, self.z + other.z) + elif isinstance(other, Vector3D): + return Point3D(self.x + other.x, self.y + other.y, self.z + other.z) + else: + raise TypeError('Cannot add Point3D and {}'.format(type(other))) + + def __sub__(self, other): + # Point - Vector -> Point + # Point - Point -> Vector + if isinstance(other, Point3D): + return Vector3D(self.x - other.x, self.y - other.y, self.z - other.z) + elif isinstance(other, Vector3D): + return Point3D(self.x - other.x, self.y - other.y, self.z - other.z) + else: + raise TypeError('Cannot subtract Point3D and {}'.format(type(other))) + + def __repr__(self): + """Point3D representation.""" + return 'Point3D (%.2f, %.2f, %.2f)' % (self.x, self.y, self.z)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/polyface.html b/docs/_modules/ladybug_geometry/geometry3d/polyface.html new file mode 100644 index 00000000..14839336 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/polyface.html @@ -0,0 +1,2100 @@ + + + + + + + ladybug_geometry.geometry3d.polyface — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.polyface

+# coding=utf-8
+"""Object with Multiple Planar Faces in 3D Space"""
+from __future__ import division
+
+from .pointvector import Vector3D, Point3D
+from .ray import Ray3D
+from .line import LineSegment3D
+from .polyline import Polyline3D
+from .plane import Plane
+from .face import Face3D
+from ._2d import Base2DIn3D
+
+try:
+    from itertools import izip as zip  # python 2
+except ImportError:
+    xrange = range  # python 3
+
+
+
+[docs] +class Polyface3D(Base2DIn3D): + """Object with Multiple Planar Faces in 3D Space. Includes solids and polyhedra. + + Args: + vertices: A list of Point3D objects representing the vertices of + this PolyFace. + face_indices: A list of lists with one list for each face of the polyface. + Each face list must contain at least one tuple of integers corresponding + to indices within the vertices list. Additional tuples of integers may + follow this one such that the first tuple denotes the boundary of the + face while each subsequent tuple denotes a hole in the face. + edge_information: Optional edge information, which will speed up the + creation of the Polyface object if it is available but should be left + as None if it is unknown. If None, edge_information will be computed + from the vertices and face_indices inputs. Edge information + should be formatted as a dictionary with two keys as follows: + + * 'edge_indices': + An array objects that each contain two integers. + These integers correspond to indices within the vertices list and + each tuple represents a line segment for an edge of the polyface. + * 'edge_types': + An array of integers for each edge that parallels the edge_indices + list. An integer of 0 denotes a naked edge, an integer of 1 + denotes an internal edge. Anything higher is a non-manifold edge. + + Properties: + * vertices + * faces + * edges + * naked_edges + * internal_edges + * non_manifold_edges + * face_indices + * edge_indices + * edge_types + * min + * max + * center + * area + * volume + * is_solid + """ + __slots__ = ('_faces', '_edges', + '_naked_edges', '_internal_edges', '_non_manifold_edges', + '_face_indices', '_edge_indices', '_edge_types', + '_area', '_volume', '_is_solid') + + def __init__(self, vertices, face_indices, edge_information=None): + """Initialize Polyface3D.""" + # assign input properties + Base2DIn3D.__init__(self, vertices) + self._face_indices = tuple(tuple(tuple(loop) for loop in face) + for face in face_indices) + + if edge_information is not None: # unpack the input edge information + edge_i = edge_information['edge_indices'] + edge_t = edge_information['edge_types'] + else: # autocalculate the edge information from the vertices and faces + edge_i = [] + edge_t = [] + for face in face_indices: + for fi in face: + for i, vi in enumerate(fi): + try: # this can get slow for large number of vertices + ind = edge_i.index((vi, fi[i - 1])) + edge_t[ind] += 1 + except ValueError: # make sure reversed edge isn't there + try: + ind = edge_i.index((fi[i - 1], vi)) + edge_t[ind] += 1 + except ValueError: # add a new edge + if fi[i - 1] != vi: # avoid cases of same start and end + edge_i.append((fi[i - 1], vi)) + edge_t.append(0) + self._edge_indices = edge_i if isinstance(edge_i, tuple) else tuple(edge_i) + self._edge_types = edge_t if isinstance(edge_t, tuple) else tuple(edge_t) + + # determine solidity of the polyface by checking for internal edges + self._is_solid = True + for edge in self._edge_types: + if edge != 1: + self._is_solid = False + break + + # assign default properties + self._faces = None + self._edges = None + self._naked_edges = None + self._internal_edges = None + self._non_manifold_edges = None + self._area = None + self._volume = None + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Face3D from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "type": "Polyface3D", + "vertices": [(0, 0, 0), (10, 0, 0), (10, 10, 0), (0, 10, 0)], + "face_indices": [[(0, 1, 2)], [(3, 0, 1)]], + "edge_information": { + "edge_indices":[(0, 1), (1, 2), (2, 0), (2, 3), (3, 0)], + "edge_types":[0, 0, 1, 0, 0] + } + } + """ + if 'edge_information' in data and data['edge_information'] is not None: + edge_information = data['edge_information'] + else: + edge_information = None + + return cls(tuple(Point3D.from_array(pt) for pt in data['vertices']), + data['face_indices'], edge_information)
+ + +
+[docs] + @classmethod + def from_faces(cls, faces, tolerance): + """Initialize Polyface3D from a list of Face3D objects. + + Note that the Polyface3D.faces property of the resulting polyface will + have an order of faces that matches the order input to this classmethod. + + Args: + faces: A list of Face3D objects representing the boundary of this Polyface. + tolerance: The maximum difference between x, y, and z values at which + the vertex of two adjacent faces is considered the same. + """ + # extract unique vertices from the faces + vertices = [] # collection of vertices as point objects + face_indices = [] # collection of face indices + for f in faces: + ind = [] + loops = (f.boundary,) if not f.has_holes else (f.boundary,) + f.holes + for j, loop in enumerate(loops): + ind.append([]) + for v in loop: + found = False + for i, vert in enumerate(vertices): + if v.is_equivalent(vert, tolerance): + found = True + ind[j].append(i) + break + if not found: # add new point + vertices.append(v) + ind[j].append(len(vertices) - 1) + face_indices.append(tuple(ind)) + + # get the polyface object and assign correct faces to it + face_obj = cls(vertices, face_indices) + if face_obj._is_solid: + face_obj._faces = cls.get_outward_faces(faces, 0.01) + else: + face_obj._faces = tuple(faces) + return face_obj
+ + +
+[docs] + @classmethod + def from_box(cls, width, depth, height, base_plane=None): + """Initialize Polyface3D from parameters describing a box. + + Initializing a polyface this way has the added benefit of having its + faces property quickly calculated. + + Args: + width: A number for the width of the box (in the X direction). + depth: A number for the depth of the box (in the Y direction). + height: A number for the height of the box (in the Z direction). + base_plane: A Plane object from which to generate the box. + If None, default is the WorldXY plane. + """ + assert isinstance(width, (float, int)), 'Box width must be a number.' + assert isinstance(depth, (float, int)), 'Box depth must be a number.' + assert isinstance(height, (float, int)), 'Box height must be a number.' + if base_plane is not None: + assert isinstance(base_plane, Plane), \ + 'base_plane must be Plane. Got {}.'.format(type(base_plane)) + else: + base_plane = Plane(Vector3D(0, 0, 1), Point3D()) + _o = base_plane.o + _w_vec = base_plane.x * width + _d_vec = base_plane.y * depth + _h_vec = base_plane.n * height + _verts = (_o, _o + _d_vec, _o + _d_vec + _w_vec, _o + _w_vec, + _o + _h_vec, _o + _d_vec + _h_vec, + _o + _d_vec + _w_vec + _h_vec, _o + _w_vec + _h_vec) + _face_indices = ([(0, 1, 2, 3)], [(2, 1, 5, 6)], [(6, 7, 3, 2)], + [(0, 3, 7, 4)], [(0, 4, 5, 1)], [(7, 6, 5, 4)]) + _edge_indices = ((3, 0), (0, 1), (1, 2), (2, 3), (0, 4), (4, 5), + (5, 1), (3, 7), (7, 4), (6, 2), (5, 6), (6, 7)) + polyface = cls(_verts, _face_indices, {'edge_indices': _edge_indices, + 'edge_types': [1] * 12}) + verts = tuple(tuple(_verts[i] for i in face[0]) for face in _face_indices) + bottom = Face3D(verts[0], base_plane.flip(), enforce_right_hand=False) + middle = tuple(Face3D(v, enforce_right_hand=False) for v in verts[1:5]) + top = Face3D(verts[5], base_plane.move(_h_vec), enforce_right_hand=False) + polyface._faces = (bottom,) + middle + (top,) + polyface._volume = width * depth * height + return polyface
+ + +
+[docs] + @classmethod + def from_offset_face(cls, face, offset): + """Initialize a solid Polyface3D from a Face3D offset along its normal. + + The resulting polyface will always be offset in the direction of + the face normal. + + When a polyface is initialized this way, the first face of the + Polyface3D.faces will always be the input face used to create the + object, the last face will be the offset version of the face, and all + other faces will form the extrusion connecting the two. + + Args: + face: A Face3D to serve as a base for the polyface. + offset: A number for the distance to offset the face to make a solid. + """ + assert isinstance(face, Face3D), \ + 'face must be a Face3D. Got {}.'.format(type(face)) + assert isinstance(offset, (float, int)), \ + 'height must be a number. Got {}.'.format(type(offset)) + # compute vertices, face indices, and edges of the extrusion + extru_vec = face.normal * offset + verts, face_ind_extru, edge_indices = \ + Polyface3D._verts_faces_edges_from_boundary(face.boundary, extru_vec) + if face.has_holes: + _st_i = len(verts) + for i, hole in enumerate(face.hole_polygon2d): + hole_verts = face._holes[i] if hole.is_clockwise else \ + tuple(reversed(face._holes[i])) + verts_2, face_ind_extru_2, edge_indices_2 = \ + Polyface3D._verts_faces_edges_from_boundary( + hole_verts, extru_vec, _st_i) + verts.extend(verts_2) + face_ind_extru.extend(face_ind_extru_2) + edge_indices.extend(edge_indices_2) + _st_i += len(hole_verts * 2) + face_ind_extru = [[fc] for fc in face_ind_extru] + # compute the final faces (accounting for top and bottom) + if not face.has_holes: + len_faces = len(face.boundary) + face_ind_bottom = [tuple(reversed(xrange(len_faces)))] + face_ind_top = [tuple( + reversed(xrange(len_faces * 2 - 1, len_faces - 1, -1)))] + else: + face_verts_bottom = [list(reversed(face.boundary))] + list(face.holes) + face_verts_top = [[pt.move(extru_vec) for pt in reversed(loop)] + for loop in face_verts_bottom] + face_ind_bottom = [tuple(verts.index(pt) for pt in loop) + for loop in face_verts_bottom] + face_ind_top = [tuple(verts.index(pt) for pt in loop) + for loop in face_verts_top] + faces_ind = [face_ind_bottom] + face_ind_extru + [face_ind_top] + # create the polyface and assign known properties. + polyface = cls(verts, faces_ind, {'edge_indices': edge_indices, + 'edge_types': [1] * len(edge_indices)}) + polyface._volume = face.area * offset + face_verts = tuple( + tuple(tuple(verts[i] for i in loop) for loop in f) for f in faces_ind) + if not face.has_holes: + polyface._faces = tuple(Face3D(v[0], enforce_right_hand=False) + for v in face_verts) + else: + mid_faces = [Face3D(v[0], enforce_right_hand=False) + for v in face_verts[1:-1]] + bottom_face = face.flip() + top_face = face.move(extru_vec) + polyface._faces = tuple([bottom_face] + mid_faces + [top_face]) + return polyface
+ + + @property + def vertices(self): + """Tuple of all vertices in this polyface. + + Note that, in the case of a polyface with holes, some vertices will be repeated + since this property effectively traces out a single boundary around the + whole shape, winding inward to cut out the holes. + """ + return self._vertices + + @property + def faces(self): + """Tuple of all Face3D objects making up this polyface.""" + if self._faces is None: + faces = [] + for face in self._face_indices: + boundary = tuple(self.vertices[i] for i in face[0]) + if len(face) == 1: + faces.append(Face3D(boundary)) + else: + holes = tuple(tuple(self.vertices[i] for i in f) for f in face[1:]) + faces.append(Face3D(boundary=boundary, holes=holes)) + if self._is_solid: + self._faces = Polyface3D.get_outward_faces(faces, 0.01) + else: + self._faces = tuple(faces) + return self._faces + + @property + def edges(self): + """"Tuple of all edges in this polyface as LineSegment3D objects.""" + if self._edges is None: + self._edges = tuple(LineSegment3D.from_end_points( + self.vertices[seg[0]], self.vertices[seg[1]]) + for seg in self._edge_indices) + return self._edges + + @property + def naked_edges(self): + """"Tuple of all naked edges in this polyface as LineSegment3D objects. + + Naked edges belong to only one face in the polyface (they are not + shared between faces). + """ + if self._naked_edges is None: + self._naked_edges = self._get_edge_type(0) + return self._naked_edges + + @property + def internal_edges(self): + """"Tuple of all internal edges in this polyface as LineSegment3D objects. + + Internal edges are shared between two faces in the polyface. + """ + if self._internal_edges is None: + self._internal_edges = self._get_edge_type(1) + return self._internal_edges + + @property + def non_manifold_edges(self): + """"Tuple of all non-manifold edges in this polyface as LineSegment3D objects. + + Non-manifold edges are shared between three or more faces and are therefore + not allowed in solid polyfaces. + """ + if self._non_manifold_edges is None: + if self._edges is None: + self.edges + nm_edges = [] + for i, type in enumerate(self._edge_types): + if type > 1: + nm_edges.append(self._edges[i]) + self._non_manifold_edges = tuple(nm_edges) + return self._non_manifold_edges + + @property + def face_indices(self): + """Tuple of face tuples with integers corresponding to indices of vertices.""" + return self._face_indices + + @property + def edge_indices(self): + """Tuple of edge tuples with integers corresponding to indices of vertices.""" + return self._edge_indices + + @property + def edge_types(self): + """Tuple of integers for each edge that denotes the type of edge. + + 0 denotes a naked edge, 1 denotes an internal edge, and anything higher is a + non-manifold edge. + """ + return self._edge_types + + @property + def edge_information(self): + """Dictionary with keys edge_indices, edge_types and corresponding properties. + """ + return {'edge_indices': self._edge_indices, 'edge_types': self._edge_types} + + @property + def area(self): + """The total surface area of the polyface.""" + if self._area is None: + self._area = sum([face.area for face in self.faces]) + return self._area + + @property + def volume(self): + """The volume enclosed by the polyface. + + Note that, if this polyface is not solid (with all face normals pointing + outward), the value of this property will not be valid. + """ + if self._volume is None: + # formula taken from https://en.wikipedia.org/wiki/Polyhedron#Volume + _v = 0 + for i, face in enumerate(self.faces): + _v += face[0].dot(face.normal) * face.area + self._volume = _v / 3 + return self._volume + + @property + def is_solid(self): + """A boolean to note whether the polyface is solid (True) or is open (False). + + Note that all solid polyface objects will have faces pointing outwards. + """ + return self._is_solid + +
+[docs] + def merge_overlapping_edges(self, tolerance, angle_tolerance): + """Get this object with overlapping naked edges merged into single internal edges + + This can be used to determine if a polyface is truly solid since this check + is not performed by default when the Polyface3D is created from_faces. + In the default test of edge conditions, overlapping colinear edges are + considered naked when the could be interpreted a single internal edge + such as the case below: + + .. code-block:: shell + + | 1 | + A|______________________|C + | B| | + | | | + | 2 | 3 | + + If Face 1 only has edge AC and not two separate edges for AB and BC, the + creation of the polyface will yield naked edges for AC, AB, and BC, meaning + the shape would not be considered solid when it might actually be so. This + merge_overlapping_edges method overcomes this by replacing the entire set + of 3 naked edges above a single internal edge running from A to C. + + Args: + tolerance: The minimum distance between a vertex and the boundary segments + at which point the vertex is considered colinear. + angle_tolerance: The max angle in radians that vertices are allowed to + differ from one another in order to consider them colinear. + """ + # get naked edges + naked_edges = list(self.naked_edges) + if len(naked_edges) == 0: + return self + + # establish lists that will be iteratively edited + remove_i = [] + add_edges = [] + naked_edge_i = [] + naked_edge_ind = [] + for i, x in enumerate(self.edge_types): + if x == 0: + naked_edge_i.append(i) + naked_edge_ind.append(self._edge_indices[i]) + + while len(naked_edge_i) > 1: + # get all of the edges that are colinear with the first edge + coll_edges = list(naked_edge_ind[0]) + coll_i = [naked_edge_i[0]] + kept_i, maybe_kept_i = [], [] + for edge, ind, nei, i in zip( + naked_edges[1:], naked_edge_ind[1:], naked_edge_i[1:], + xrange(1, len(naked_edges))): + try: + if edge.is_colinear(naked_edges[0], tolerance, angle_tolerance): + coll_edges.extend(ind) + coll_i.append(nei) + maybe_kept_i.append(i) + else: + kept_i.append(i) + except ZeroDivisionError: # duplicate vertices resulted in 0 length edge + coll_edges.extend(ind) + coll_i.append(nei) + maybe_kept_i.append(i) + + # determine if colinear edges create a full double line along the edge + if len(coll_edges) == 1: # definitely a naked edge + overlapping = False + else: # all colinear edges are likely a part of the same loop + final_vi = [] + coll_edges_sort = sorted(coll_edges) + overlapping = True + for i in range(0, len(coll_edges_sort), 2): + final_vi.append(coll_edges_sort[i]) + if coll_edges_sort[i] != coll_edges_sort[i + 1]: + overlapping = False + break + # there's still a chance we have multiple colinear loops + if not overlapping: + dup = {x for x in coll_edges if coll_edges.count(x) > 1} + if coll_edges[0] in dup and coll_edges[1] in dup: + rebuilt_edges = [(coll_edges[i], coll_edges[i + 1]) + for i in range(0, len(coll_edges), 2)] + loop_i = set(rebuilt_edges[0]) + edge_to_check = rebuilt_edges[1:] + more_to_check = True + while more_to_check: + for i, r_seg in enumerate(edge_to_check): + if (r_seg[0] in loop_i or r_seg[1] in loop_i): + loop_i.add(r_seg[0]) + loop_i.add(r_seg[1]) + del edge_to_check[i] + break + else: + more_to_check = False + if all(li in dup for li in loop_i): # we have found a loop! + final_vi = list(loop_i) + new_coll_i = [coll_i[0]] + zip_obj = zip(rebuilt_edges[1:], coll_i[1:], maybe_kept_i) + for ind, nei, i in zip_obj: + if ind[0] in loop_i: + new_coll_i.append(nei) + else: + kept_i.append(i) + kept_i.sort() + coll_i = new_coll_i + overlapping = True + + # if fully overlapping edges have been found, remake them into one + if overlapping: + remove_i.extend(coll_i) # remove overlapping edges from the list + verts = [self.vertices[j] for j in final_vi] + dir_vec = verts[0] - verts[1] + if dir_vec.x != 0: + vert_coor = [v.x for v in verts] + elif dir_vec.y != 0: + vert_coor = [v.y for v in verts] + else: + vert_coor = [v.z for v in verts] + vert_coor, final_vi = zip(*sorted(zip(vert_coor, final_vi))) + add_edges.append((final_vi[0], final_vi[-1])) + + # delete the colinear vertices that have been accounted for + naked_edges = [naked_edges[i] for i in kept_i] + naked_edge_ind = [naked_edge_ind[i] for i in kept_i] + naked_edge_i = [naked_edge_i[i] for i in kept_i] + + # create the new edge information and the new polyface + new_edge_indices = list(self.edge_indices) + new_edge_types = list(self.edge_types) + add_i = [] + for i in range(len(new_edge_indices)): + if i not in remove_i: + add_i.append(i) + new_edge_indices = [new_edge_indices[i] for i in add_i] + new_edge_types = [new_edge_types[i] for i in add_i] + for new_edge in add_edges: + new_edge_indices.append(new_edge) + new_edge_types.append(1) + _new_polyface = Polyface3D( + self._vertices, self._face_indices, + {'edge_indices': new_edge_indices, 'edge_types': new_edge_types} + ) + + # if the result is still not solid, perform a check on the remaining edges + if len(_new_polyface.naked_edges) != 0 and \ + len(_new_polyface.non_manifold_edges) == 0: + # it's possible that the remaining gaps are smaller than the tolerance + small_gaps = True + joined_edges = Polyline3D.join_segments(_new_polyface.naked_edges, tolerance) + for bnd in joined_edges: + if isinstance(bnd, Polyline3D) and bnd.is_closed(tolerance): + test_face = Face3D(bnd.vertices) + if test_face.check_planar(tolerance, False): + min_, max_ = test_face.min, test_face.max + max_dim = max(max_.x - min_.x, max_.y - min_.y, max_.z - min_.z) + tol_area = tolerance * max_dim + if test_face.area > tol_area: + small_gaps = False + break + else: + small_gaps = False + break + else: + small_gaps = False + break + if small_gaps: # the gaps are small enough to make the Polyface closed + new_edge_types = [1 for _ in new_edge_types] + _new_polyface = Polyface3D( + self._vertices, self._face_indices, + {'edge_indices': new_edge_indices, 'edge_types': new_edge_types} + ) + + return _new_polyface
+ + +
+[docs] + def move(self, moving_vec): + """Get a polyface that has been moved along a vector. + + Args: + moving_vec: A Vector3D with the direction and distance to move the polyface. + """ + _verts = tuple(pt.move(moving_vec) for pt in self.vertices) + _new_pface = Polyface3D(_verts, self.face_indices, self.edge_information) + if self._faces is not None: + _new_pface._faces = tuple(face.move(moving_vec) for face in self._faces) + _new_pface._volume = self._volume + return _new_pface
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate a polyface by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + _verts = tuple(pt.rotate(axis, angle, origin) for pt in self.vertices) + _new_pface = Polyface3D(_verts, self.face_indices, self.edge_information) + if self._faces is not None: + _new_pface._faces = tuple(face.rotate(axis, angle, origin) + for face in self._faces) + _new_pface._volume = self._volume + return _new_pface
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a polyface rotated counterclockwise in the world XY plane by an angle. + + Args: + angle: An angle in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + _verts = tuple(pt.rotate_xy(angle, origin) for pt in self.vertices) + _new_pface = Polyface3D(_verts, self.face_indices, self.edge_information) + if self._faces is not None: + _new_pface._faces = tuple(face.rotate_xy(angle, origin) + for face in self._faces) + _new_pface._volume = self._volume + return _new_pface
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a polyface reflected across a plane with the input normal and origin. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the polyface will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin from which to reflect. + """ + _verts = tuple(pt.reflect(normal, origin) for pt in self.vertices) + _new_pface = Polyface3D(_verts, self.face_indices, self.edge_information) + if self._faces is not None: + _new_pface._faces = tuple(face.reflect(normal, origin) + for face in self._faces) + _new_pface._volume = self._volume + return _new_pface
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a polyface by a factor from an origin point. + + Args: + factor: A number representing how much the polyface should be scaled. + origin: A Point3D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + if origin is None: + _verts = tuple(Point3D(pt.x * factor, pt.y * factor, pt.z * factor) + for pt in self._vertices) + else: + _verts = tuple(pt.scale(factor, origin) for pt in self.vertices) + _new_pface = Polyface3D(_verts, self.face_indices, self.edge_information) + if self._faces is not None: + _new_pface._faces = tuple(face.scale(factor, origin) + for face in self._faces) + _new_pface._volume = self._volume * factor ** 3 \ + if self._volume is not None else None + return _new_pface
+ + +
+[docs] + def is_point_inside(self, point, test_vector=Vector3D(1, 0, 0)): + """Test whether a Point3D lies inside or outside the polyface. + + Note that, if this polyface is not solid, the result will always be False. + + Args: + point: A Point3D for which the inside/outside relationship will be tested. + test_vector: Optional vector to set the direction in which intersections + with the polyface faces will be evaluated to determine if the + point is inside. Default is the X-unit vector. + + Returns: + A boolean denoting whether the point lies inside (True) or outside (False). + """ + if not self.is_solid: + return False + test_ray = Ray3D(point, test_vector) + n_int = 0 + for _f in self.faces: + if _f.intersect_line_ray(test_ray): + n_int += 1 + if n_int % 2 == 0: + return False + return True
+ + +
+[docs] + def does_intersect_line_ray_exist(self, line_ray): + """Boolean for whether an intersection exists between the input Line3D or Ray3D. + + Args: + line_ray: A Line3D or Ray3D object for which intersection will be evaluated. + + Returns: + True if an intersection exists. False if it does not exist. + """ + for face in self.faces: + _int = face.intersect_line_ray(line_ray) + if _int is not None: + return True + return False
+ + +
+[docs] + def intersect_line_ray(self, line_ray): + """Get the intersections between this polyface and the input Line3D or Ray3D. + + Args: + line_ray: A Line3D or Ray3D object for which intersection will be computed. + + Returns: + A list of Point3D for the intersection. Will be an empty list if no + intersection exists. + """ + _inters = [] + for face in self.faces: + _int = face.intersect_line_ray(line_ray) + if _int is not None: + _inters.append(_int) + return _inters
+ + +
+[docs] + def intersect_plane(self, plane): + """Get the intersection between this polyface and the input plane. + + Args: + plane: A Plane object for which intersection will be computed. + + Returns: + List of LineSegment3D objects for the intersection. + Will be an empty list if no intersection exists. + """ + _inters = [] + for face in self.faces: + _int = face.intersect_plane(plane) + if _int is not None: + _inters.extend(_int) + return _inters
+ + +
+[docs] + @staticmethod + def overlapping_bounding_boxes(polyface1, polyface2, tolerance): + """Check if the bounding boxes of two polyfaces overlap within a tolerance. + + This is particularly useful as a check before performing computationally + intense processes between two polyfaces like intersection or checking for + adjacency. Checking the overlap of the bounding boxes is extremely quick + given this method's use of the Separating Axis Theorem. + + Args: + polyface1: The first polyface to check. + polyface2: The second polyface to check. + tolerance: Distance within which two points are considered to be co-located. + """ + # Bounding box check using the Separating Axis Theorem + polyf1_width = polyface1.max.x - polyface1.min.x + polyf2_width = polyface2.max.x - polyface2.min.x + dist_btwn_x = abs(polyface1.center.x - polyface2.center.x) + x_gap_btwn_box = dist_btwn_x - (0.5 * polyf1_width) - (0.5 * polyf2_width) + + polyf1_depth = polyface1.max.y - polyface1.min.y + polyf2_depth = polyface2.max.y - polyface2.min.y + dist_btwn_y = abs(polyface1.center.y - polyface2.center.y) + y_gap_btwn_box = dist_btwn_y - (0.5 * polyf1_depth) - (0.5 * polyf2_depth) + + polyf1_height = polyface1.max.z - polyface1.min.z + polyf2_height = polyface2.max.z - polyface2.min.z + dist_btwn_z = abs(polyface1.center.z - polyface2.center.z) + z_gap_btwn_box = dist_btwn_z - (0.5 * polyf1_height) - (0.5 * polyf2_height) + + if x_gap_btwn_box > tolerance or y_gap_btwn_box > tolerance or \ + z_gap_btwn_box > tolerance: + return False # no overlap + return True # overlap exists
+ + +
+[docs] + @staticmethod + def get_outward_faces(faces, tolerance): + """Turn a list of faces forming a solid into one where they all point outward. + + Note that, if the input faces do not form a closed solid, there may be some + output faces that are not pointing outward. However, if the gaps in the + combined solid are within the input tolerance, this should not be an issue. + + Also, note that this method runs automatically for any solid polyface + (meaning every solid polyface automatically has outward-facing faces). So there + is no need to rerun this method for faces from a solid polyface. + + Args: + faces: A list of Face3D objects that together form a solid. + tolerance: Optional tolerance for the permissable size of gap between + faces at which point the faces are considered to have a single edge. + + Returns: + outward_faces -- A list of the input Face3D objects that all point outwards + (provided the input faces form a solid). + """ + outward_faces = [] + for i, face in enumerate(faces): + # construct a ray with the face normal and a point on the face + point_on_face = face._point_on_face(tolerance) + test_ray = Ray3D(point_on_face, face.normal) + + # if the ray intersects with an even number of other faces, it is correct + n_int = 0 + for _f in faces[i + 1:]: + if _f.intersect_line_ray(test_ray): + n_int += 1 + for _f in faces[:i]: + if _f.intersect_line_ray(test_ray): + n_int += 1 + if n_int % 2 == 0: + outward_faces.append(face) + else: + outward_faces.append(face.flip()) + return outward_faces
+ + +
+[docs] + def to_dict(self, include_edge_information=True): + """Get Polyface3D as a dictionary. + + Args: + include_edge_information: Set to True to include the edge_information + in the dictionary, which will allow for fast initialization when + it is de-serialized. Default True. + """ + base = {'type': 'Polyface3D', + 'vertices': [v.to_array() for v in self.vertices], + 'face_indices': self.face_indices} + if include_edge_information: + base['edge_information'] = self.edge_information + return base
+ + + def _get_edge_type(self, edge_type): + """Get all of the edges of a certain type in this polyface.""" + if self._edges is None: + self.edges + sel_edges = [] + for i, type in enumerate(self._edge_types): + if type == edge_type: + sel_edges.append(self._edges[i]) + return tuple(sel_edges) + + @staticmethod + def _verts_faces_edges_from_boundary(cclock_verts, extru_vec, st_i=0): + """Get vertices and face indices for a given Face3D loop (boundary or hole).""" + verts = list(cclock_verts) + [pt.move(extru_vec) for pt in cclock_verts] + len_faces = len(cclock_verts) + faces_ind = [] + for i in xrange(st_i, st_i + len_faces - 1): + faces_ind.append((i, i + 1, i + len_faces + 1, i + len_faces)) + faces_ind.append((st_i + len_faces - 1, st_i, + st_i + len_faces, st_i + len_faces * 2 - 1)) + edge_i1 = [(st_i + i, st_i + i + 1) for i in xrange(len_faces - 1)] + edge_i2 = [(st_i + i, st_i + i + len_faces) for i in xrange(len_faces)] + edge_i3 = [(st_i + len_faces + i, st_i + len_faces + i + 1) + for i in xrange(len_faces - 1)] + edge_indices = edge_i1 + [(st_i + len_faces - 1, st_i)] + edge_i2 + edge_i3 + \ + [(st_i + len_faces * 2 - 1, st_i + len_faces)] + return verts, faces_ind, edge_indices + + def __copy__(self): + _new_poly = Polyface3D(self.vertices, self.face_indices, self.edge_information) + _new_poly._faces = self._faces + return _new_poly + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return tuple(hash(pt) for pt in self._vertices) + \ + tuple(hash(face) for face in self._face_indices) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Polyface3D) and self.__key() == other.__key() + + def __repr__(self): + return 'Polyface3D ({} faces) ({} vertices)'.format( + len(self.faces), len(self))
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/polyline.html b/docs/_modules/ladybug_geometry/geometry3d/polyline.html new file mode 100644 index 00000000..97a20e56 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/polyline.html @@ -0,0 +1,1536 @@ + + + + + + + ladybug_geometry.geometry3d.polyline — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.polyline

+# coding=utf-8
+"""3D Polyline"""
+from __future__ import division
+
+from ..geometry2d.pointvector import Point2D
+from ..geometry2d.polyline import Polyline2D
+
+from ._2d import Base2DIn3D
+from .pointvector import Point3D
+from .line import LineSegment3D
+from .plane import Plane
+from ..intersection3d import intersect_line3d_plane
+from .._polyline import _group_vertices
+
+
+
+[docs] +class Polyline3D(Base2DIn3D): + """3D polyline object. + + Args: + vertices: A list of Point3D objects representing the vertices of the polyline. + interpolated: Boolean to note whether the polyline should be interpolated + between the input vertices when it is translated to other interfaces. + Note that this property has no bearing on the geometric calculations + performed by this library and is only present in order to assist with + display/translation. + + Properties: + * vertices + * segments + * min + * max + * center + * p1 + * p2 + * length + * interpolated + """ + __slots__ = ('_interpolated', '_segments', '_length') + + def __init__(self, vertices, interpolated=False): + """Initialize Polyline3D.""" + Base2DIn3D.__init__(self, vertices) + self._interpolated = interpolated + self._segments = None + self._length = None + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Polyline3D from a dictionary. + + Args: + data: A python dictionary in the following format. + + .. code-block:: python + + { + "type": "Polyline3D", + "vertices": [(0, 0, 0), (10, 0, 2), (0, 10, 4)] + } + """ + interp = data['interpolated'] if 'interpolated' in data else False + return cls(tuple(Point3D.from_array(pt) for pt in data['vertices']), interp)
+ + +
+[docs] + @classmethod + def from_array(cls, point_array): + """Create a Polyline3D from a nested array of vertex coordinates. + + Args: + point_array: nested array of point arrays. + """ + return Polyline3D(Point3D(*point) for point in point_array)
+ + +
+[docs] + @classmethod + def from_polyline2d(cls, polyline2d, plane=None): + """Create a closed Polyline3D from a Polyline2D and a plane. + + Args: + polyline2d: A Polyline2D object to be converted to a Polyline3D. + plane: A Plane in which the Polyline2D sits. If None, the WorldXY + plane will be used. + """ + plane = Plane() if plane is None else plane + return Polyline3D((plane.xy_to_xyz(pt) for pt in polyline2d.vertices), + polyline2d.interpolated)
+ + + @property + def segments(self): + """Tuple of all line segments in the polyline.""" + if self._segments is None: + self._segments = \ + tuple(LineSegment3D.from_end_points(vert, self._vertices[i + 1]) + for i, vert in enumerate(self._vertices[:-1])) + return self._segments + + @property + def p1(self): + """Starting point of the Polyline3D.""" + return self._vertices[0] + + @property + def p2(self): + """End point of the Polyline3D.""" + return self._vertices[-1] + + @property + def length(self): + """The length of the polyline.""" + if self._length is None: + self._length = sum([seg.length for seg in self.segments]) + return self._length + + @property + def interpolated(self): + """Boolean noting whether the polyline should be interpolated upon translation. + + Note that this property has no bearing on the geometric calculations + performed by this library and is only present in order to assist with + display/translation. + """ + return self._interpolated + +
+[docs] + def is_closed(self, tolerance): + """Test whether this polyline is closed to within the tolerance. + + Args: + tolerance: The minimum difference between vertices below which vertices + are considered the same. + """ + return self._vertices[0].is_equivalent(self._vertices[-1], tolerance)
+ + +
+[docs] + def remove_colinear_vertices(self, tolerance): + """Get a version of this polyline without colinear or duplicate vertices. + + Args: + tolerance: The minimum distance that a vertex can be from a line + before it is considered colinear. + """ + if len(self.vertices) == 3: + return self # Polyline3D cannot have fewer than 3 vertices + new_vertices = [self.vertices[0]] # first vertex is always ok + for i, _v in enumerate(self.vertices[1:-1]): + if (self[i] - _v).cross(self[i + 2] - _v).magnitude >= tolerance: + new_vertices.append(_v) + new_vertices.append(self[-1]) # last vertex is always ok + _new_poly = Polyline3D(new_vertices) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def reverse(self): + """Get a copy of this polyline where the vertices are reversed.""" + _new_poly = Polyline3D(tuple(pt for pt in reversed(self.vertices))) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def move(self, moving_vec): + """Get a polyline that has been moved along a vector. + + Args: + moving_vec: A Vector3D with the direction and distance to move the polyline. + """ + _new_poly = Polyline3D(tuple(pt.move(moving_vec) for pt in self.vertices)) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate a polyline by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the point will be rotated. + """ + _new_poly = Polyline3D(tuple(pt.rotate(axis, angle, origin) + for pt in self.vertices)) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a polyline rotated counterclockwise in the XY plane by a certain angle. + + Args: + angle: An angle in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + _new_p = Polyline3D(tuple(pt.rotate_xy(angle, origin) for pt in self.vertices)) + self._transfer_properties(_new_p) + return _new_p
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a polyline reflected across a plane with the input normal and origin. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the polyline will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin from which to reflect. + """ + _new_poly = Polyline3D(tuple(pt.reflect(normal, origin) for pt in self.vertices)) + self._transfer_properties(_new_poly) + return _new_poly
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a polyline by a factor from an origin point. + + Args: + factor: A number representing how much the polyline should be scaled. + origin: A Point3D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + if origin is None: + _new_poly = Polyline3D(tuple( + Point3D(pt.x * factor, pt.y * factor, pt.z * factor) + for pt in self.vertices)) + else: + _new_poly = Polyline3D(tuple( + pt.scale(factor, origin) for pt in self.vertices)) + _new_poly._interpolated = self._interpolated + return _new_poly
+ + +
+[docs] + def intersect_plane(self, plane): + """Get the intersections between this polyline and a Plane. + + Args: + plane: A Plane that will be intersected with this object. + + Returns: + A list with Point3D objects for the intersections. + List will be empty if no intersection exists. + """ + intersections = [] + for _s in self.segments: + inters = intersect_line3d_plane(_s, plane) + if inters is not None: + intersections.append(inters) + return intersections
+ + +
+[docs] + def split_with_plane(self, plane): + """Split this Polyline3D into Polyline3Ds and LineSegment3Ds using a Plane. + + Args: + plane: A Plane that will be used to split this polyline. + + Returns: + A list of Polyline3D and LineSegment3D objects if the split was successful. + Will be a list with 1 Polyline3D if no intersection exists. + """ + # group the vertices based on when they cross the plane + grouped_verts = [[self._vertices[0]]] + for _s in self.segments: + inters = intersect_line3d_plane(_s, plane) + if inters is None: + grouped_verts[-1].append(_s.p2) + else: # intersection; start a new group + grouped_verts[-1].append(inters) + grouped_verts.append([inters, _s.p2]) + + # make new Polyline3D and LineSegment3D objects based on the groups + return self._grouped_verts_to_objs(grouped_verts, self._interpolated)
+ + +
+[docs] + def to_dict(self): + """Get Polyline3D as a dictionary.""" + base = {'type': 'Polyline3D', + 'vertices': [pt.to_array() for pt in self.vertices]} + if self.interpolated: + base['interpolated'] = self.interpolated + return base
+ + +
+[docs] + def to_array(self): + """Get a list of lists where each sub-list represents a Point3D vertex.""" + return tuple(pt.to_array() for pt in self.vertices)
+ + +
+[docs] + def to_polyline2d(self): + """Get a Polyline2D in the XY plane derived from this 3D polyline.""" + return Polyline2D( + (Point2D(pt.x, pt.y) for pt in self.vertices), self.interpolated)
+ + +
+[docs] + @staticmethod + def join_segments(segments, tolerance): + """Get an array of Polyline3Ds from a list of LineSegment3Ds. + + Args: + segments: An array of LineSegment3D objects. + tolerance: The minimum difference in X, Y, and Z values at which Point2Ds + are considered equivalent. Segments with points that match within the + tolerance will be joined. + + Returns: + An array of Polyline3D and LineSegment3D objects assembled from the + joined segments. + """ + # group the vertices that make up polylines + grouped_verts = _group_vertices(segments, tolerance) + # create the Polyline3D and LineSegment3D objects + return Polyline3D._grouped_verts_to_objs(grouped_verts)
+ + + def _transfer_properties(self, new_polyline): + """Transfer properties from this polyline to a new polyline.""" + new_polyline._interpolated = self._interpolated + new_polyline._length = self._length + + @staticmethod + def _grouped_verts_to_objs(grouped_verts, interpolated=False): + joined_lines = [] + for v_list in grouped_verts: + if len(v_list) == 2: + joined_lines.append(LineSegment3D.from_end_points(v_list[0], v_list[1])) + else: + joined_lines.append(Polyline3D(v_list, interpolated)) + return joined_lines + + def __copy__(self): + return Polyline3D(self._vertices, self._interpolated) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return tuple(hash(pt) for pt in self._vertices) + (self._interpolated,) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Polyline3D) and self.__key() == other.__key() + + def __repr__(self): + return 'Polyline3D ({} vertices)'.format(len(self))
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/ray.html b/docs/_modules/ladybug_geometry/geometry3d/ray.html new file mode 100644 index 00000000..7cf61c11 --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/ray.html @@ -0,0 +1,1331 @@ + + + + + + + ladybug_geometry.geometry3d.ray — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.ray

+# coding=utf-8
+"""3D Ray"""
+from __future__ import division
+
+from .pointvector import Point3D, Vector3D
+from ._1d import Base1DIn3D
+
+
+
+[docs] +class Ray3D(Base1DIn3D): + """3D Ray object. + + Args: + p: A Point3D representing the base of the ray. + v: A Vector3D representing the direction of the ray. + + Properties: + * p + * v + * min + * max + * center + """ + __slots__ = () + + def __init__(self, p, v): + """Initialize Ray3D.""" + Base1DIn3D.__init__(self, p, v) + +
+[docs] + @classmethod + def from_array(cls, ray_array): + """ Create a Ray3D from a nested array with a point and a vector. + + Args: + ray_array: Nested tuples ((p.x, p.y), (v.x, v.y)). + """ + return Ray3D(Point3D(*ray_array[0]), Vector3D(*ray_array[1]))
+ + +
+[docs] + @classmethod + def from_ray2d(cls, ray2d, z=0): + """Initialize a new Ray3D from an Ray2D and a z value. + + Args: + line2d: A Ray2D to be used to generate the Ray3D. + z: A number for the Z coordinate value of the line. + """ + base_p = Point3D(ray2d.p.x, ray2d.p.y, z) + base_v = Vector3D(ray2d.v.x, ray2d.v.y, 0) + return cls(base_p, base_v)
+ + +
+[docs] + def reverse(self): + """Get a copy of this ray that is reversed.""" + return Ray3D(self.p, self.v.reverse())
+ + +
+[docs] + def move(self, moving_vec): + """Get a ray that has been moved along a vector. + + Args: + moving_vec: A Vector3D with the direction and distance to move the ray. + """ + return Ray3D(self.p.move(moving_vec), self.v)
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate a ray by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + return Ray3D(self.p.rotate(axis, angle, origin), self.v.rotate(axis, angle))
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a ray rotated counterclockwise in the XY plane by a certain angle. + + Args: + angle: An angle in radians. + origin: A Point3D for the origin around which the object will be rotated. + """ + return Ray3D(self.p.rotate_xy(angle, origin), self.v.rotate_xy(angle))
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a ray reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the ray will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin from which to reflect. + """ + return Ray3D(self.p.reflect(normal, origin), self.v.reflect(normal))
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a ray by a factor from an origin point. + + Args: + factor: A number representing how much the ray should be scaled. + origin: A Point3D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + return Ray3D(self.p.scale(factor, origin), self.v * factor)
+ + +
+[docs] + def scale_world_origin(self, factor): + """Scale a ray by a factor from the world origin. Faster than Ray2D.scale. + + Args: + factor: A number representing how much the ray should be scaled. + """ + return Ray3D(self.p.scale_world_origin(factor), self.v * factor)
+ + +
+[docs] + def to_dict(self): + """Get Ray3D as a dictionary.""" + base = Base1DIn3D.to_dict(self) + base['type'] = 'Ray3D' + return base
+ + +
+[docs] + def to_array(self): + """A nested array representing the start point and vector.""" + return (self.p.to_array(), self.v.to_array())
+ + + def _u_in(self, u): + return u >= 0.0 + + def __copy__(self): + return Ray3D(self.p, self.v) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (hash(self.p), hash(self.v)) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Ray3D) and self.__key() == other.__key() + + def __repr__(self): + return 'Ray3D (point <%.2f, %.2f, %.2f>) (vector <%.2f, %.2f, %.2f>)' % \ + (self.p.x, self.p.y, self.p.z, self.v.x, self.v.y, self.v.z)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/geometry3d/sphere.html b/docs/_modules/ladybug_geometry/geometry3d/sphere.html new file mode 100644 index 00000000..b2a96e6d --- /dev/null +++ b/docs/_modules/ladybug_geometry/geometry3d/sphere.html @@ -0,0 +1,1404 @@ + + + + + + + ladybug_geometry.geometry3d.sphere — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.geometry3d.sphere

+# coding=utf-8
+"""Sphere"""
+from __future__ import division
+
+from .pointvector import Point3D
+from .plane import Plane
+from .arc import Arc3D
+from .line import LineSegment3D
+from ..intersection3d import intersect_line3d_sphere, intersect_plane_sphere
+
+
+import math
+
+
+
+[docs] +class Sphere(object): + """Sphere object. + + Args: + center: A Point3D representing the center of the arc. + radius: A number representing the radius of the sphere. + + Properties: + * center + * radius + * min + * max + * diameter + * circumference + * area + * volume + """ + __slots__ = ('_center', '_radius') + + def __init__(self, center, radius): + """Initialize Sphere.""" + assert isinstance(center, Point3D), \ + "Expected Point3D. Got {}.".format(type(center)) + assert radius > 0, 'Sphere radius must be greater than 0. Got {}.'.format(radius) + self._center = center + self._radius = radius + +
+[docs] + @classmethod + def from_dict(cls, data): + """Create a Sphere from a dictionary. + + Args: + data: A python dictionary in the following format + + .. code-block:: python + + { + "type": "Sphere" + "center": (10, 0, 0), + "radius": 5 + } + """ + return cls(Point3D.from_array(data['center']), data['radius'])
+ + + @property + def center(self): + """Center of sphere.""" + return self._center + + @property + def radius(self): + """Radius of sphere.""" + return self._radius + + @property + def min(self): + """A Point3D for the minimum bounding box vertex around this geometry.""" + return Point3D(self.center.x - self.radius, self.center.y - self.radius, + self.center.z - self.radius) + + @property + def max(self): + """A Point3D for the maximum bounding box vertex around this geometry.""" + return Point3D(self.center.x + self.radius, self.center.y + self.radius, + self.center.z + self.radius) + + @property + def diameter(self): + """Diameter of sphere""" + return self.radius * 2 + + @property + def circumference(self): + """Circumference of sphere""" + return 2 * math.pi * self.radius + + @property + def area(self): + """Surface area of sphere""" + return 4 * math.pi * self.radius ** 2 + + @property + def volume(self): + """Volume of sphere""" + return 4 / 3 * math.pi * self.radius ** 3 + +
+[docs] + def move(self, moving_vec): + """Get a sphere that has been moved along a vector. + + Args: + moving_vec: A Vector3D with the direction and distance to move the sphere. + """ + return Sphere(self.center.move(moving_vec), self.radius)
+ + +
+[docs] + def rotate(self, axis, angle, origin): + """Rotate this sphere by a certain angle around an axis and origin. + + Right hand rule applies: + If axis has a positive orientation, rotation will be clockwise. + If axis has a negative orientation, rotation will be counterclockwise. + + Args: + axis: A Vector3D axis representing the axis of rotation. + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the sphere will be rotated. + """ + return Sphere(self.center.rotate(axis, angle, origin), self.radius)
+ + +
+[docs] + def rotate_xy(self, angle, origin): + """Get a sphere that is rotated counterclockwise in the world XY plane by an angle. + + Args: + angle: An angle for rotation in radians. + origin: A Point3D for the origin around which the sphere will be rotated. + """ + return Sphere(self.center.rotate_xy(angle, origin), self.radius)
+ + +
+[docs] + def reflect(self, normal, origin): + """Get a sphere reflected across a plane with the input normal vector and origin. + + Args: + normal: A Vector3D representing the normal vector for the plane across + which the arc will be reflected. THIS VECTOR MUST BE NORMALIZED. + origin: A Point3D representing the origin from which to reflect. + """ + return Sphere(self.center.reflect(normal, origin), self.radius)
+ + +
+[docs] + def scale(self, factor, origin=None): + """Scale a sphere by a factor from an origin point. + + Args: + factor: A number representing how much the sphere should be scaled. + origin: A Point3D representing the origin from which to scale. + If None, it will be scaled from the World origin (0, 0, 0). + """ + return Sphere(self.center.scale(factor, origin), self.radius * factor)
+ + +
+[docs] + def intersect_plane(self, plane): + """Get the intersection of a plane with this Sphere object + + Args: + plane: A Plane object. + + Returns: + Arc3D representing a full circle if it exists. + None if no full intersection exists. + """ + ip = intersect_plane_sphere(plane, self) # ip = [center pt, vector, radius] + return None if ip is None or isinstance(ip, Point3D) else \ + Arc3D(Plane(ip[1], ip[0]), ip[2])
+ + +
+[docs] + def intersect_line_ray(self, line_ray): + """Get the intersection between this Sphere object and a Ray2D/LineSegment2D. + + Args: + line_ray: A LineSegment3D or Ray3D that will be extended infinitely + for intersection. + + Returns: + A LineSegment3D object if a full intersection exists. + A Point if a tangent intersection exists. + None if no full intersection exists. + """ + il = intersect_line3d_sphere(line_ray, self) + return None if il is None else \ + il if isinstance(il, Point3D) else \ + LineSegment3D.from_end_points(il[0], il[1])
+ + +
+[docs] + def duplicate(self): + """Get a copy of this object.""" + return self.__copy__()
+ + +
+[docs] + def to_dict(self): + """Get Sphere as a dictionary.""" + return {'type': 'Sphere', + 'center': self.center.to_array(), + 'radius': self.radius}
+ + + def __copy__(self): + return Sphere(self._center, self._radius) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self._center, self._radius) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Sphere) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + +
+[docs] + def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__()
+ + + def __repr__(self): + return 'Sphere (center {}) (radius {}))'.format(self.center, self.radius)
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/interop/obj.html b/docs/_modules/ladybug_geometry/interop/obj.html new file mode 100644 index 00000000..7865bf48 --- /dev/null +++ b/docs/_modules/ladybug_geometry/interop/obj.html @@ -0,0 +1,1690 @@ + + + + + + + ladybug_geometry.interop.obj — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.interop.obj

+# coding=utf-8
+"""A class that supports the import and export of OBJ data to/from ladybug_geometry.
+"""
+import os
+
+try:
+    from itertools import izip as zip  # python 2
+    writemode = 'wb'
+except ImportError:
+    writemode = 'w'  # python 3
+
+from ladybug_geometry.geometry2d.pointvector import Point2D
+from ladybug_geometry.geometry3d.pointvector import Vector3D, Point3D
+
+
+
+[docs] +class OBJ(object): + """A class that supports the import and export of OBJ data to/from ladybug_geometry. + + Note that ladybug_geometry Mesh3D can be easily created from this OBJ by + taking the vertices and normals. + + Args: + vertices: A list or tuple of Point3D objects for vertices. + faces: A list of tuples with each tuple having either 3 or 4 integers. + These integers correspond to indices within the list of vertices. + vertex_texture_map: An optional list or tuple of Point2D that align with the + vertices input. All coordinate values of the Point2D should be between + 0 and 1 and are intended to map to the XY system of images to be mapped + onto the OBJ mesh. If None, the OBJ file is written without + textures. (Default: None). + vertex_normals: An optional list or tuple of Vector3D that align with the + vertices input and describe the normal vector to be used at each vertex. + If None, the OBJ file is written without normals. (Default: None). + vertex_colors: An optional list of colors that align with the vertices input. + Note that these are written into the OBJ alongside the vertex + coordinates separately from the texture map. Not all programs support + importing OBJs with this color information but Rhino does. (Default: None). + material_structure: A list of tuples where each tuple contains two elements. + The first is the identifier of a material that is used in the OBJ and + the second is the index of the face where the application of the new + material begins. If None, everything will be assumed to have the + same diffuse material. (Default: None). + + Properties: + * vertices + * faces + * vertex_texture_map + * vertex_normals + * vertex_colors + * material_structure + """ + + __slots__ = ( + '_vertices', '_faces', '_vertex_texture_map', '_vertex_normals', + '_vertex_colors', '_material_structure' + ) + + def __init__( + self, vertices, faces, vertex_texture_map=None, vertex_normals=None, + vertex_colors=None, material_structure=None + ): + self._vertices = self._check_vertices_input(vertices) + self._faces = self._check_faces_input(faces) + self.vertex_texture_map = vertex_texture_map + self.vertex_normals = vertex_normals + self.vertex_colors = vertex_colors + self.material_structure = material_structure + +
+[docs] + @classmethod + def from_file(cls, file_path): + """Create an OBJ object from a .obj file. + + Args: + file_path: Path to an OBJ file as a text string. Note that, if the file + includes texture mapping coordinates or vertex normals, the number + of texture coordinates and normals must align with the number of + vertices to be importable. Nearly all OBJ files follow this standard. + If any of the OBJ mesh faces contain more than 4 vertices, only + the first 4 vertices will be counted. + """ + vertices, faces, vertex_texture_map, vertex_normals, vertex_colors = \ + [], [], [], [], [] + mat_struct = [] + with open(file_path, 'r') as fp: + for line in fp: + if line.startswith('#'): + continue + wds = line.split() + if len(wds) > 0: + first_word = wds[0] + if first_word == 'v': # start of a new vertex + vert = Point3D(float(wds[1]), float(wds[2]), float(wds[3])) + vertices.append(vert) + if len(wds) > 4: + vertex_colors.append(tuple(wds[4:])) + elif first_word == 'f': # start of a new face + face = [] + for fv in wds[1:]: + face.append(int(fv.split('/')[0]) - 1) + if len(face) > 4: # truncate for compatibility with Mesh3D + face = face[:4] + faces.append(tuple(face)) + elif first_word == 'vn': # start of a new vertex normal + norm = Vector3D(float(wds[1]), float(wds[2]), float(wds[3])) + vertex_normals.append(norm) + elif first_word == 'vt': # start of a new texture coordinate + texture = Point2D(float(wds[1]), float(wds[2])) + vertex_texture_map.append(texture) + elif first_word == 'usemtl': # start of a new material application + mat_struct.append((wds[1], len(faces))) + return cls(vertices, faces, vertex_texture_map, vertex_normals, + vertex_colors, mat_struct)
+ + +
+[docs] + @classmethod + def from_mesh3d(cls, mesh, include_colors=True, include_normals=False): + """Create an OBJ object from a ladybug_geometry Mesh3D. + + If colors are specified on the Mesh3D, they will be correctly transferred + to the resulting OBJ object as long as include_colors is True. + + Args: + mesh: A ladybug_geometry Mesh3D object to be converted to an OBJ object. + include_colors: Boolean to note whether the Mesh3D colors should be + transferred to the OBJ object. (Default: True). + include_normals: Boolean to note whether the vertex normals should be + included in the resulting OBJ object. (Default: False). + """ + if include_colors and mesh.is_color_by_face: + # we need to duplicate vertices to preserve colors + vertices, faces, colors = [], [], [] + v_ct = 0 + for face_verts, col in zip(mesh.face_vertices, mesh.colors): + vertices.extend(face_verts) + if len(face_verts) == 4: + faces.append((v_ct, v_ct + 1, v_ct + 2, v_ct + 3)) + colors.extend([col] * 4) + v_ct += 4 + else: + faces.append((v_ct, v_ct + 1, v_ct + 2)) + colors.extend([col] * 3) + v_ct += 3 + if include_normals: + msh_norms = mesh.vertex_normals + vert_normals = [] + for face in mesh.faces: + for fi in face: + vert_normals.append(msh_norms[fi]) + return cls(vertices, faces, vertex_normals=msh_norms, + vertex_colors=colors) + return cls(vertices, faces, vertex_colors=colors) + vertex_colors = mesh.colors if include_colors else None + if include_normals: + return cls(mesh.vertices, mesh.faces, vertex_normals=mesh.vertex_normals, + vertex_colors=vertex_colors) + return cls(mesh.vertices, mesh.faces, vertex_colors=vertex_colors)
+ + +
+[docs] + @classmethod + def from_mesh3ds(cls, meshes, material_ids=None, include_normals=False): + """Create an OBJ object from a list of ladybug_geometry Mesh3D. + + Mesh3D colors are ignored when using this method with the assumption that + materials are used to specify how the meshes should be rendered. + + Args: + meshes: A list of ladybug_geometry Mesh3D objects to be converted + into an OBJ object. + material_ids: An optional list of strings that aligns with the input + meshes and denote materials assigned to each mesh. This list of + material IDs will be automatically converted into an efficient + material_structure for the OBJ object where materials used for + multiple meshes only include one reference to the material. If + None, the OBJ will have no material structure. (Default: None). + include_normals: Boolean to note whether the vertex normals should be + included in the resulting OBJ object. (Default: False). + """ + # sort the meshes by material ID to ensure efficient material structure + if material_ids is not None: + assert len(material_ids) == len(meshes), 'Length of OBJ material_ids ({}) ' \ + 'does not match the length of meshes ({}).'.format( + len(material_ids), len(meshes)) + meshes = [x for _, x in sorted(zip(material_ids, meshes))] + material_ids = sorted(material_ids) + + # gather all vertices, faces, and (optionally) normals together + vertices, faces, normals, mat_struct = [], [], [], [] + v_count = 0 + if material_ids is not None: + last_mat = None + for mesh, mat_id in zip(meshes, material_ids): + if mat_id != last_mat: + mat_struct.append((mat_id, len(faces))) + last_mat = mat_id + vertices.extend(mesh.vertices) + if include_normals: + normals.extend(mesh.vertex_normals) + if v_count == 0: + faces.extend(mesh.faces) + else: + for f in mesh.faces: + faces.append(tuple(fi + v_count for fi in f)) + v_count += len(mesh.vertices) + else: + for mesh in meshes: + vertices.extend(mesh.vertices) + if include_normals: + normals.extend(mesh.vertex_normals) + if v_count == 0: + faces.extend(mesh.faces) + else: + for f in mesh.faces: + faces.append(tuple(fi + v_count for fi in f)) + v_count += len(mesh.vertices) + + return cls( + vertices, faces, vertex_normals=normals, material_structure=mat_struct)
+ + + @property + def vertices(self): + """Tuple of Point3D for all vertices in the OBJ.""" + return self._vertices + + @property + def faces(self): + """Tuple of tuples for all faces in the OBJ.""" + return self._faces + + @property + def vertex_texture_map(self): + """Get or set a tuple of Point2D for texture image coordinates for each vertex. + + Will be None if no texture map is assigned. + """ + return self._vertex_texture_map + + @vertex_texture_map.setter + def vertex_texture_map(self, value): + if value is not None: + assert isinstance(value, (list, tuple)), 'vertex_texture_map should be ' \ + 'a list or tuple. Got {}'.format(type(value)) + if isinstance(value, list): + value = tuple(value) + if len(value) == 0: + value = None + elif len(value) != len(self.vertices): + raise ValueError( + 'Number of items in vertex_texture_map ({}) does not match number' + 'of OBJ vertices ({}).'.format(len(value), len(self.vertices))) + else: + for vert in value: + assert isinstance(vert, Point2D), 'Expected Point2D for OBJ ' \ + 'vertex texture. Got {}.'.format(type(vert)) + self._vertex_texture_map = value + + @property + def vertex_normals(self): + """Get or set a tuple of Vector3D for vertex normals. + + Will be None if no vertex normals are assigned. + """ + return self._vertex_normals + + @vertex_normals.setter + def vertex_normals(self, value): + if value is not None: + assert isinstance(value, (list, tuple)), \ + 'vertex_normals should be a list or tuple. Got {}'.format(type(value)) + if isinstance(value, list): + value = tuple(value) + if len(value) == 0: + value = None + elif len(value) != len(self.vertices): + raise ValueError( + 'Number of OBJ vertex_normals ({}) does not match the number of' + ' OBJ vertices ({}).'.format(len(value), len(self.vertices))) + else: + for norm in value: + assert isinstance(norm, Vector3D), 'Expected Vector3D for OBJ ' \ + 'vertex normal. Got {}.'.format(type(norm)) + self._vertex_normals = value + + @property + def vertex_colors(self): + """Get or set a list of colors for the OBJ. Will be None if no colors assigned. + """ + return self._vertex_colors + + @vertex_colors.setter + def vertex_colors(self, value): + if value is not None: + assert isinstance(value, (list, tuple)), \ + 'vertex_normals should be a list or tuple. Got {}'.format(type(value)) + if isinstance(value, list): + value = tuple(value) + if len(value) == 0: + value = None + elif len(value) != len(self.vertices): + raise ValueError( + 'Number of OBJ vertex_normals ({}) does not match the number of' + ' OBJ vertices ({}).'.format(len(value), len(self.vertices))) + self._vertex_colors = value + + @property + def material_structure(self): + """Get or set a tuple of tuples that specify the material structure of the obj. + + Each sub-tuple contains two elements. The first is the identifier of a + material that is used in the OBJ and the second is the index of the face + where the application of the new material begins. If None, everything + will be assumed to have the same diffuse material. + """ + return self._material_structure + + @material_structure.setter + def material_structure(self, value): + if value is not None: + assert isinstance(value, (list, tuple)), \ + 'vertex_normals should be a list or tuple. Got {}'.format(type(value)) + if len(value) == 0: + value = None + else: + for mt in value: + assert isinstance(mt, tuple), 'Expected tuple for OBJ material ' \ + 'structure. Got {}.'.format(type(mt)) + assert len(mt) == 2, 'OBJ material structure must have 2 items. ' \ + 'Got {}.'.format(len(mt)) + assert isinstance(mt[0], str), 'Expected String for OBJ material ' \ + 'identifier. Got {}.'.format(type(mt[0])) + try: + self._faces[mt[1]] + except IndexError: + raise IndexError( + 'OBJ material index {} does not correspond to any face. ' + 'There are {} faces in the mesh.'.format( + mt[1], len(self._faces))) + except TypeError: + raise TypeError( + 'OBJ material must use integers to reference faces. ' + 'Got {}.'.format(type(mt[1]))) + value = sorted(value, key=lambda x: x[1]) + value = tuple(value) + self._material_structure = value + +
+[docs] + def to_file(self, folder, name, triangulate_quads=False, include_mtl=False): + """Write the OBJ object to an ASCII text file. + + Args: + folder: A text string for the directory where the OBJ will be written. + name: A text string for the name of the OBJ file. Note that, if an image + texture is meant to be assigned to this OBJ, the image should have + the same name as the one input here except with the .mtl extension + instead of the .obj extension. + triangulate_quads: Boolean to note whether quad faces should be + triangulated upon export to OBJ. This may be needed for certain + software platforms that require the mesh to be composed entirely + of triangles (eg. Radiance). (Default: False). + include_mtl: Boolean to note whether an .mtl file should be automatically + generated from the material structure written next to the .obj + file in the output folder. All materials in the mtl file will + be diffuse white, with the assumption that these will be + customized later. (Default: False). + """ + # set up a name and folder + file_name = name if name.lower().endswith('.obj') else '{}.obj'.format(name) + obj_file = os.path.join(folder, file_name) + mtl_file = '{}.mtl'.format(name) if not name.lower().endswith('.obj') else \ + '{}.mtl'.format(name[:-4]) + + # write everything into the OBJ file + with open(obj_file, writemode) as outfile: + # add a comment at the top to note where the OBJ is written from + outfile.write('# OBJ file written by ladybug geometry\n\n') + + # add material file name if include_mtl is true + if self._material_structure is not None or include_mtl: + if include_mtl: + outfile.write('mtllib ' + mtl_file + '\n') + if self._material_structure is None: + outfile.write('usemtl diffuse_0\n') + + # loop through the vertices and add them to the file + if self.vertex_colors is None: + for v in self.vertices: + outfile.write('v {} {} {}\n'.format(v.x, v.y, v.z)) + else: # write the vertex colors alongside the vertices + if len(self.vertex_colors[0]) > 3: + for v, c in zip(self.vertices, self.vertex_colors): + outfile.write( + 'v {} {} {} {} {} {}\n'.format( + v.x, v.y, v.z, c[0], c[1], c[2]) + ) + else: # might be a grayscale weight + for v, c in zip(self.vertices, self.vertex_colors): + outfile.write( + 'v {} {} {} {}\n'.format(v.x, v.y, v.z, ' '.join(c)) + ) + + # loop through the texture vertices, if present, and add them to the file + if self.vertex_texture_map is not None: + for vt in self.vertex_texture_map: + outfile.write('vt {} {}\n'.format(vt.x, vt.y)) + + # loop through the normals, if present, and add them to the file + if self.vertex_normals is not None: + for vn in self.vertex_normals: + outfile.write('vn {} {} {}\n'.format(vn.x, vn.y, vn.z)) + + # triangulate the faces if requested + formatted_faces, formatted_mats = self.faces, self.material_structure + if triangulate_quads: + formatted_faces = [] + if formatted_mats is None or len(formatted_mats) == 1: + for f in self.faces: + if len(f) > 3: + formatted_faces.append((f[0], f[1], f[2])) + formatted_faces.append((f[2], f[3], f[0])) + else: + formatted_faces.append(f) + else: + mat_ind = [mat[1] for mat in formatted_mats] + for i, f in enumerate(self.faces): + if len(f) > 3: + formatted_faces.append((f[0], f[1], f[2])) + formatted_faces.append((f[2], f[3], f[0])) + for j, m in enumerate(formatted_mats): + if m[1] > i: + mat_ind[j] = mat_ind[j] + 1 + else: + formatted_faces.append(f) + formatted_mats = \ + [(mn[0], mi) for mn, mi in zip(formatted_mats, mat_ind)] + + # loop through the faces and get all lines of text for them + face_txt = [] + if self.vertex_texture_map is None and self.vertex_normals is None: + for f in formatted_faces: + face_txt.append('f ' + ' '.join(str(fi + 1) for fi in f) + '\n') + else: + if self.vertex_texture_map is not None and \ + self.vertex_normals is not None: + f_map = '{0}/{0}/{0}' + elif self.vertex_texture_map is None and \ + self.vertex_normals is not None: + f_map = '{0}//{0}' + else: + f_map = '{0}/{0}' + for f in formatted_faces: + face_txt.append( + 'f ' + ' '.join(f_map.format(fi + 1) for fi in f) + '\n' + ) + + # write the faces into the file with the material structure + if formatted_mats is not None: # insert the materials + for mat in reversed(formatted_mats): + face_txt.insert(mat[1], 'usemtl {}\n'.format(mat[0])) + for f_lin in face_txt: + outfile.write(f_lin) + + # write the MTL file if requested + if include_mtl: + mat_struct = [('diffuse_0', 0)] if self._material_structure is None else \ + self._material_structure + mtl_fp = os.path.join(folder, mtl_file) + with open(mtl_fp, writemode) as mtl_f: + mtl_f.write('# Ladybug Geometry\n') + for mat in reversed(mat_struct): + mtl_str = \ + 'newmtl {}\n' \ + 'Ka 0.0000 0.0000 0.0000\n' \ + 'Kd 1.0000 1.0000 1.0000\n' \ + 'Ks 0.0000 0.0000 0.0000\n' \ + 'Tf 0.0000 0.0000 0.0000\n' \ + 'd 1.0000\n' \ + 'Ns 0.0000\n'.format(mat[0]) + mtl_f.write(mtl_str) + + return obj_file
+ + + def _check_vertices_input(self, vertices): + """Check the input vertices.""" + if not isinstance(vertices, tuple): + vertices = tuple(vertices) + for vert in vertices: + assert isinstance(vert, Point3D), \ + 'Expected Point3D for OBJ vertex. Got {}.'.format(type(vert)) + return vertices + + def _check_faces_input(self, faces): + """Check input faces for correct formatting.""" + if not isinstance(faces, tuple): + faces = tuple(faces) + assert len(faces) > 0, 'OBJ mesh must have at least one face.' + for f in faces: + assert isinstance(f, tuple), \ + 'Expected tuple for Mesh face. Got {}.'.format(type(f)) + assert len(f) >= 3, \ + 'OBJ mesh face must have 3 or more vertices. Got {}.'.format(len(f)) + for ind in f: + try: + self._vertices[ind] + except IndexError: + raise IndexError( + 'mesh face index {} does not correspond to any vertex. There ' + 'are {} vertices in the mesh.'.format(ind, len(self._vertices))) + except TypeError: + raise TypeError( + 'Mesh face must use integers to reference vertices. ' + 'Got {}.'.format(type(ind))) + return faces + + def __len__(self): + return len(self._vertices) + + def __getitem__(self, key): + return self._vertices[key] + + def __iter__(self): + return iter(self._vertices) + + def __repr__(self): + return 'OBJ ({} vertices) ({} faces)'.format( + len(self._vertices), len(self._faces))
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/interop/stl.html b/docs/_modules/ladybug_geometry/interop/stl.html new file mode 100644 index 00000000..67646d6e --- /dev/null +++ b/docs/_modules/ladybug_geometry/interop/stl.html @@ -0,0 +1,1476 @@ + + + + + + + ladybug_geometry.interop.stl — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.interop.stl

+# coding=utf-8
+"""A class that supports the import and export of STL data to/from ladybug_geometry.
+
+The methods in the object below are inspired from pySTL module.
+
+[1] Daniel Balzerson. 2013. pySTL - Python code for working with .STL
+(sterolithography) files. https://github.com/proverbialsunrise/pySTL
+"""
+import os
+import struct
+import re
+
+try:
+    from itertools import izip as zip  # python 2
+    writemode = 'wb'
+except ImportError:
+    writemode = 'w'  # python 3
+
+from ladybug_geometry.geometry3d.pointvector import Vector3D, Point3D
+
+
+
+[docs] +class STL(object): + """A class that supports the import and export of STL data to/from ladybug_geometry. + + Args: + face_vertices: A list of tuples where each tuple is a triangular face + of three Point3Ds. + face_normals: A list of Vector3Ds for the normals of the faces in the STL. + name: Text string for the name of the solid object in the STL + file. (Default: polyhedron). + + Properties: + * name + * face_vertices + * face_normals + """ + + __slots__ = ('_name', '_face_vertices', '_face_normals') + + def __init__(self, face_vertices, face_normals, name='polyhedron'): + self.name = name + self._face_normals = None # bypass check on first time + self.face_vertices = face_vertices + self.face_normals = face_normals + +
+[docs] + @classmethod + def from_file(cls, file_path): + """Create an STL object from a .stl file. + + Args: + file_path: Path to an STL file as a text string. The STL file can be + in either ASCII or binary format. + """ + face_vertices, face_normals, name = cls._load_stl(file_path) + return cls(face_vertices, face_normals, name)
+ + +
+[docs] + @classmethod + def from_mesh3d(cls, mesh, name='polyhedron'): + """Create an STL object from a ladybug_geometry Mesh3D object. + + All quad faces will be automatically triangulated by using this method. + + Args: + mesh: A ladybug_geometry Mesh3D object to be converted to an OBJ object. + name: Text string for the name of the solid object in the STL + file. (Default: polyhedron). + """ + face_vertices, face_normals = [], [] + for f, fn in zip(mesh.faces, mesh.face_normals): + if len(f) == 3: + face_vertices.append(tuple(mesh._vertices[i] for i in f)) + face_normals.append(fn) + else: # it's a quad mesh to be triangulated + vts1 = (mesh._vertices[f[0]], mesh._vertices[f[1]], mesh._vertices[f[2]]) + vts2 = (mesh._vertices[f[2]], mesh._vertices[f[3]], mesh._vertices[f[0]]) + face_vertices.append(vts1) + face_vertices.append(vts2) + face_normals.append(fn) + face_normals.append(fn) + return cls(face_vertices, face_normals, name)
+ + + @property + def name(self): + """Get the name of the solid object in the STL file.""" + return self._name + + @name.setter + def name(self, value): + input_name = 'STL object name' + try: + non_ascii = tuple(i for i in value if ord(i) >= 128) + except TypeError: + raise TypeError('Input {} must be a text string. Got {}: {}.'.format( + input_name, type(value), value)) + assert non_ascii == (), 'Illegal characters {} found in {}'.format( + non_ascii, input_name) + illegal_match = re.search(r'[,;!\n\t]', value) + assert illegal_match is None, 'Illegal character "{}" found in {}'.format( + illegal_match.group(0), input_name) + assert len(value) > 0, 'Input {} "{}" contains no characters.'.format( + input_name, value) + assert len(value) <= 80, 'Input {} "{}" must be less than 80 characters.'.format( + input_name, value) + self._name = value + + @property + def face_vertices(self): + """Get a list of tuples where each tuple is a triangular face of three Point3Ds. + """ + return self._face_vertices + + @face_vertices.setter + def face_vertices(self, val): + assert isinstance(val, (list, tuple)), \ + 'face_vertices should be a list or tuple. Got {}'.format(type(val)) + if isinstance(val, list): + val = tuple(val) + for f in val: + assert len(f) == 3, 'All face_vertices of an STL must be triangles. ' \ + 'Got face of length {}.'.format(len(f)) + self._face_vertices = val + self._check_faces_match() + + @property + def face_normals(self): + """Get a list of Vector3Ds for the normals of the faces in the STL.""" + return self._face_normals + + @face_normals.setter + def face_normals(self, val): + assert isinstance(val, (list, tuple)), \ + 'face_normals should be a list or tuple. Got {}'.format(type(val)) + if isinstance(val, list): + val = tuple(val) + self._face_normals = val + self._check_faces_match() + +
+[docs] + def to_file(self, folder, name=None): + """Write the STL object to an ASCII STL file. + + Args: + folder: A text string for the directory where the STL will be written. + name: A text string for the name of the STL file. If None, the name + of the STL object will be used. (Default: None). + """ + # set up a name and folder + if name is None: + name = self.name + file_name = name if name.lower().endswith('.stl') else '{}.stl'.format(name) + stl_file = os.path.join(folder, file_name) + # loop through the faces and normals to write them to the file + with open(stl_file, writemode) as fp: + fp.write('solid {:s}\n'.format(self.name)) + for facet, nm in zip(self.face_vertices, self.face_normals): + fp.write( + ' facet normal {0:.6E} {1:.6E} {2:.6E}\n'.format(nm.x, nm.y, nm.z) + ) + fp.write(' outer loop\n') + for pt in facet: + fp.write( + ' vertex {0:.6E} {1:.6E} {2:.6E}\n'.format(pt.x, pt.y, pt.z) + ) + fp.write(' endloop\n') + fp.write(' endfacet\n') + fp.write('endsolid {:s}\n'.format(self.name)) + return stl_file
+ + + def _check_faces_match(self): + if self._face_normals is not None: + assert len(self._face_vertices) == len(self._face_normals), \ + 'Number of STL face_vertices ({}) does not match the number ' \ + 'of _face_normals ({}).'.format( + len(self._face_vertices), len(self._face_normals)) + + @staticmethod + def _load_stl(file_path): + """Load data from an STL file. + + Args: + file_path: Path to an STL file as a text string. The STL file can be + in either ASCII or binary format. + + Returns: + A tuple with three elements. + + - face_vertices: + A list of tuples where each tuple is a triangular face of three Point3Ds. + + - face_normals: + A list of Vector3Ds for the normals of the faces in the STL. + + - name: + Text string for the name of the STL object + """ + # check the first bytes of the file to determine whether it's ASCII or binary + assert os.path.isfile(file_path), 'Failed to find %s' % file_path + with open(file_path, 'rb') as fp: + header = fp.read(80) # 80 characters should have the full name + first_word = header[0:5] + # load the STL data depending on whether it is ASCII or binary + if first_word.decode('utf-8') == 'solid': + return STL._load_text_stl(file_path) + else: + return STL._load_binary_stl(file_path) + + @staticmethod + def _load_text_stl(file_path): + """Read text stl file and extract triangular faces.""" + _face_vertices, _face_normals, _name = [], [], 'polyhedron' + with open(file_path, 'r') as fp: + for line in fp: + words = line.split() + if len(words) > 0: + first_word = words[0] + if first_word == 'facet': # start of a new face + vertices = [] + norm = Vector3D( + float(words[2]), float(words[3]), float(words[4]) + ) + _face_normals.append(norm) + elif first_word == 'vertex': # vertex of a face + vertices.append( + Point3D(float(words[1]), float(words[2]), float(words[3])) + ) + elif first_word == 'endloop': # end of a face + _face_vertices.append(tuple(vertices)) + elif first_word == 'solid': # very start of the file + try: + _name = words[1] + except IndexError: # no name assigned; leave the default one + pass + return _face_vertices, _face_normals, _name + + @staticmethod + def _load_binary_stl(file_path): + """Read binary stl file and extract triangular faces.""" + _face_vertices, _face_normals, _name = [], [], 'polyhedron' + with open(file_path, 'rb') as fp: + # interpret the 80-character header as the name of the object + _name = fp.read(80).decode('utf-8').strip() + # ignore the total face count in the first 4 characters + struct.unpack('I', fp.read(4))[0] + + # loop through the file contents and load all vertices and vectors + count = 0 + while True: + try: + # read the face normal + p = fp.read(12) + if len(p) == 12: + norm = Vector3D( + struct.unpack('f', p[0:4])[0], + struct.unpack('f', p[4:8])[0], + struct.unpack('f', p[8:12])[0] + ) + else: + break + # read the first vertex + p = fp.read(12) + if len(p) == 12: + p1 = Point3D( + struct.unpack('f', p[0:4])[0], + struct.unpack('f', p[4:8])[0], + struct.unpack('f', p[8:12])[0] + ) + else: + break + # read the second vertex + p = fp.read(12) + if len(p) == 12: + p2 = Point3D( + struct.unpack('f', p[0:4])[0], + struct.unpack('f', p[4:8])[0], + struct.unpack('f', p[8:12])[0] + ) + else: + break + # read the third vertex + p = fp.read(12) + if len(p) == 12: + p3 = Point3D( + struct.unpack('f', p[0:4])[0], + struct.unpack('f', p[4:8])[0], + struct.unpack('f', p[8:12])[0] + ) + else: + break + # add the triangle to the face vertices + _face_vertices.append((p1, p2, p3)) + _face_normals.append(norm) + count += 1 + fp.read(2) + + if len(p) == 0: + break # no more points to read + except EOFError: + break # we have reached the end of the file + return _face_vertices, _face_normals, _name + + def __len__(self): + return len(self._face_vertices) + + def __getitem__(self, key): + return self._face_vertices[key] + + def __iter__(self): + return iter(self._face_vertices) + + def __repr__(self): + return 'STL ({} faces)'.format(len(self._face_vertices))
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/intersection2d.html b/docs/_modules/ladybug_geometry/intersection2d.html new file mode 100644 index 00000000..5781ca87 --- /dev/null +++ b/docs/_modules/ladybug_geometry/intersection2d.html @@ -0,0 +1,1560 @@ + + + + + + + ladybug_geometry.intersection2d — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.intersection2d

+# coding=utf-8
+"""Utility functions for computing intersections between geometry in 2D space.
+
+Taken mostly from the euclid package available at
+https://pypi.org/project/euclid/
+"""
+from __future__ import division
+import math
+
+from .geometry2d.pointvector import Point2D, Vector2D
+
+
+def _isclose(a, b, rel_tol=1e-09, abs_tol=1e-09):
+    """Implementation of the math.isclose method from Python 3.5 onward."""
+    return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
+
+
+
+[docs] +def intersect_line2d(line_ray_a, line_ray_b): + """Get the intersection between any Ray2D or LineSegment2D objects as a Point2D. + + This function calculates scaling parameters for ua and ub where: + A.p + ua * A.v = B.p + ub * B.v + Which represents the intersection point between line A and line B. + + The derivation of ua is achieved by crossing both sides of the above equation + with the direction vector of B, and rearranging the formula: + + .. code-block:: python + + A.p + ua * A.v = B.p + ub * B.v + (A.p + ua * A.v) x B.v = (B.p + ub * B.v) x B.v # Cross both sides with B.v + (A.p x B.v) + (ua * A.v x B.v) = (B.p x B.v) + (ub * B.v x B.v) # B.v x B.v = 0 + ua = (B.p - A.p) x B.v / (A.v x B.v) + + Args: + line_ray_a: A LineSegment2D or Ray2D object. + line_ray_b: Another LineSegment2D or Ray2D to intersect. + + Returns: + Point2D of intersection if it exists. None if no intersection exists. + """ + # d is the determinant between lines, if 0 lines are collinear + d = line_ray_b.v.y * line_ray_a.v.x - line_ray_b.v.x * line_ray_a.v.y + if d == 0: + return None + + # (dx, dy) = A.p - B.p + dy = line_ray_a.p.y - line_ray_b.p.y + dx = line_ray_a.p.x - line_ray_b.p.x + + # Find parameters ua and ub for intersection between two lines + + # Calculate scaling parameter for line_ray_b + ua = (line_ray_b.v.x * dy - line_ray_b.v.y * dx) / d + # Checks the bounds of ua to ensure it obeys ray/line behavior + if not line_ray_a._u_in(ua): + return None + + # Calculate scaling parameter for line_ray_b + ub = (line_ray_a.v.x * dy - line_ray_a.v.y * dx) / d + # Checks the bounds of ub to ensure it obeys ray/line behavior + if not line_ray_b._u_in(ub): + return None + + return Point2D(line_ray_a.p.x + ua * line_ray_a.v.x, + line_ray_a.p.y + ua * line_ray_a.v.y)
+ + + +
+[docs] +def intersect_line_segment2d(line_a, line_b): + """Get the intersection between two LineSegment2D objects as a Point2D. + + This function is identical to intersect_line2d but has some extra checks to + avoid certain cases of floating point tolerance issues. It is only intended + to work with LineSegment2D and not Ray2D. + + Args: + line_a: A LineSegment2D object. + line_b: Another LineSegment2D intersect. + + Returns: + Point2D of intersection if it exists. None if no intersection exists. + """ + # d is the determinant between lines, if 0 lines are collinear + d = line_b.v.y * line_a.v.x - line_b.v.x * line_a.v.y + if d == 0: + return None + + # (dx, dy) = A.p - B.p + dy = line_a.p.y - line_b.p.y + dx = line_a.p.x - line_b.p.x + + # Find parameters ua and ub for intersection between two lines + + # Calculate scaling parameter for line_b + ua = (line_b.v.x * dy - line_b.v.y * dx) / d + # Checks the bounds of ua to ensure it obeys ray/line behavior + if not line_a._u_in(ua): + return None + + # Calculate scaling parameter for line_b + ub = (line_a.v.x * dy - line_a.v.y * dx) / d + # Checks the bounds of ub to ensure it obeys ray/line behavior + if not line_b._u_in(ub): + return None + + # compute the intersection point + int_pta = Point2D(line_a.p.x + ua * line_a.v.x, line_a.p.y + ua * line_a.v.y) + int_ptb = Point2D(line_b.p.x + ub * line_b.v.x, line_b.p.y + ub * line_b.v.y) + + # if the two points are unequal, there's a floating point tolerance issue + if _isclose(int_pta.x, int_ptb.x) and _isclose(int_pta.y, int_ptb.y): + return int_pta + return None
+ + + +
+[docs] +def intersect_line2d_infinite(line_ray_a, line_ray_b): + """Get intersection between a Ray2D/LineSegment2D and another extended infinitely. + + Args: + line_ray_a: A LineSegment2D or Ray2D object. + line_ray_b: A LineSegment2D or Ray2D that will be extended infinitely + for intersection. + + Returns: + Point2D of intersection if it exists. None if no intersection exists. + """ + d = line_ray_b.v.y * line_ray_a.v.x - line_ray_b.v.x * line_ray_a.v.y + if d == 0: + return None + dy = line_ray_a.p.y - line_ray_b.p.y + dx = line_ray_a.p.x - line_ray_b.p.x + ua = (line_ray_b.v.x * dy - line_ray_b.v.y * dx) / d + if not line_ray_a._u_in(ua): + return None + return Point2D(line_ray_a.p.x + ua * line_ray_a.v.x, + line_ray_a.p.y + ua * line_ray_a.v.y)
+ + + +
+[docs] +def does_intersection_exist_line2d(line_ray_a, line_ray_b): + """Boolean denoting whether an intersection exists between Ray2D or LineSegment2D. + + This is slightly faster than actually computing the intersection but should only be + used in cases where the actual point of intersection is not needed. + + Args: + line_ray_a: A LineSegment2D or Ray2D object. + line_ray_b: Another LineSegment2D or Ray2D to intersect. + + Returns: + True if an intersection exists. False if no intersection exists. + """ + d = line_ray_b.v.y * line_ray_a.v.x - line_ray_b.v.x * line_ray_a.v.y + if d == 0: + return False + dy = line_ray_a.p.y - line_ray_b.p.y + dx = line_ray_a.p.x - line_ray_b.p.x + ua = (line_ray_b.v.x * dy - line_ray_b.v.y * dx) / d + if not line_ray_a._u_in(ua): + return False + ub = (line_ray_a.v.x * dy - line_ray_a.v.y * dx) / d + if not line_ray_b._u_in(ub): + return False + return True
+ + + +
+[docs] +def intersect_line2d_arc2d(line_ray, arc): + """Get the intersection between any Ray2D/LineSegment2D and an Arc2D. + + Args: + line_ray: A LineSegment2D or Ray2D object. + arc: An Arc2D object along which the closest point will be determined. + + Returns: + A list of 2 Point2D objects if a full intersection exists. + A list with a single Point2D object if the line is tangent or intersects + only once. None if no intersection exists. + """ + a = line_ray.v.magnitude_squared + b = 2 * (line_ray.v.x * (line_ray.p.x - arc.c.x) + + line_ray.v.y * (line_ray.p.y - arc.c.y)) + c = arc.c.magnitude_squared + line_ray.p.magnitude_squared - \ + 2 * arc.c.dot(line_ray.p) - arc.r ** 2 + det = b ** 2 - 4 * a * c + if det < 0: + return None + sq = math.sqrt(det) + u1 = (-b + sq) / (2 * a) + u2 = (-b - sq) / (2 * a) + pt1 = Point2D(line_ray.p.x + u1 * line_ray.v.x, + line_ray.p.y + u1 * line_ray.v.y) if line_ray._u_in(u1) else None + pt2 = Point2D(line_ray.p.x + u2 * line_ray.v.x, + line_ray.p.y + u2 * line_ray.v.y) if line_ray._u_in(u2) else None + + if u1 == u2: # Tangent + pt = Point2D(line_ray.p.x + u1 * line_ray.v.x, + line_ray.p.y + u1 * line_ray.v.y) + return pt if arc._pt_in(pt) else None + + pts = [p for p in (pt1, pt2) if p is not None and arc._pt_in(p)] + return pts if len(pts) != 0 else None
+ + + +
+[docs] +def intersect_line2d_infinite_arc2d(line_ray, arc): + """Get intersection between an Arc2D and a Ray2D/LineSegment2D extended infinitely. + + Args: + line_ray: A LineSegment2D or Ray2D that will be extended infinitely + for intersection. + arc: An Arc2D object along which the closest point will be determined. + + Returns: + A list of 2 Point2D objects if a full intersection exists. + A list with a single Point2D object if the line is tangent or intersects + only once. None if no intersection exists. + """ + a = line_ray.v.magnitude_squared + b = 2 * (line_ray.v.x * (line_ray.p.x - arc.c.x) + + line_ray.v.y * (line_ray.p.y - arc.c.y)) + c = arc.c.magnitude_squared + line_ray.p.magnitude_squared - \ + 2 * arc.c.dot(line_ray.p) - arc.r ** 2 + det = b ** 2 - 4 * a * c + if det < 0: + return None + sq = math.sqrt(det) + u1 = (-b + sq) / (2 * a) + u2 = (-b - sq) / (2 * a) + + if u1 == u2: # Tangent + pt = Point2D(line_ray.p.x + u1 * line_ray.v.x, line_ray.p.y + u1 * line_ray.v.y) + return pt if arc._pt_in(pt) else None + + pt1 = Point2D(line_ray.p.x + u1 * line_ray.v.x, line_ray.p.y + u1 * line_ray.v.y) + pt2 = Point2D(line_ray.p.x + u2 * line_ray.v.x, line_ray.p.y + u2 * line_ray.v.y) + pts = [p for p in (pt1, pt2) if arc._pt_in(p)] + return pts if len(pts) != 0 else None
+ + + +
+[docs] +def closest_point2d_on_line2d(point, line_ray): + """Get the closest Point2D on a LineSegment2D or Ray2D to the input point. + + Args: + point: A Point2D object. + line_ray: A LineSegment2D or Ray2D object along which the closest point + will be determined. + + Returns: + Point2D for the closest point on the line_ray to point. + """ + d = line_ray.v.magnitude_squared + if d == 0: # zero-length segment; just return the end point + return line_ray.p + u = ((point.x - line_ray.p.x) * line_ray.v.x + + (point.y - line_ray.p.y) * line_ray.v.y) / d + if not line_ray._u_in(u): + u = max(min(u, 1.0), 0.0) + return Point2D(line_ray.p.x + u * line_ray.v.x, line_ray.p.y + u * line_ray.v.y)
+ + + +
+[docs] +def closest_point2d_on_line2d_infinite(point, line_ray): + """Get the closest Point2D on a Ray2D/LineSegment2D extended infinitely. + + Args: + point: A Point2D object. + line_ray: A LineSegment2D or Ray2D object that will be extended infinitely + to determine where the closest point lies. + + Returns: + Point2D for the closest point on the line_ray to point. + """ + d = line_ray.v.magnitude_squared + if d == 0: # zero-length segment; just return the end point + return line_ray.p + u = ((point.x - line_ray.p.x) * line_ray.v.x + + (point.y - line_ray.p.y) * line_ray.v.y) / d + return Point2D(line_ray.p.x + u * line_ray.v.x, line_ray.p.y + u * line_ray.v.y)
+ + + +
+[docs] +def closest_point2d_between_line2d(line_ray_a, line_ray_b): + """Get the two closest Point2D between two LineSegment2D objects. + + When the closest point on one of the segments lies in the middle of the + segment, this will be accounted for. Also note that the line segments + should not intersect for the result to be valid. + + Args: + line_ray_a: A LineSegment2D object. + line_ray_b: Another LineSegment2D to which closest points will + be determined. + + Returns: + A tuple with two elements + + - dists[0]: The distance between the two LineSegment2D objects. + - pts[0]: A tuple of two Point2D objects representing: + + 1) The point on line_ray_a that is closest to line_ray_b + 2) The point on line_ray_b that is closest to line_ray_a + """ + # one of the 4 endpoints must be a closest point + pt_1 = closest_point2d_on_line2d(line_ray_a.p, line_ray_b) + dist_1 = pt_1.distance_to_point(line_ray_a.p) + a_p2 = line_ray_a.p2 + pt_2 = closest_point2d_on_line2d(a_p2, line_ray_b) + dist_2 = pt_2.distance_to_point(a_p2) + pt_3 = closest_point2d_on_line2d(line_ray_b.p, line_ray_a) + dist_3 = pt_3.distance_to_point(line_ray_b.p) + b_p2 = line_ray_b.p2 + pt_4 = closest_point2d_on_line2d(b_p2, line_ray_a) + dist_4 = pt_4.distance_to_point(b_p2) + + # sort the closest points based on their distance + dists = [dist_1, dist_2, dist_3, dist_4] + pts = [(line_ray_a.p, pt_1), (a_p2, pt_2), (pt_3, line_ray_b.p), (pt_4, b_p2)] + dists, i = zip(*sorted(zip(dists, range(len(pts))))) + return dists[0], pts[i[0]]
+ + + +
+[docs] +def closest_end_point2d_between_line2d(line_a, line_b): + """Get the two closest end Point2Ds between two LineSegment2D objects. + + The result will always be composed of endpoints of the segments and will not + account for cases where the closest point lies in the middle of a segment. + For cases where such middle points are important, the + closest_point2d_between_line2d method should be used. + + Args: + line_a: A LineSegment2D object. + line_b: Another LineSegment2D to which closest points will + be determined. + + Returns: + A tuple with two elements + + - dist: The distance between the two endpoints objects. + - pts: A tuple of two Point2D objects representing: + + 1) The end point on line_a that is closest to line_b + 2) The end point on line_b that is closest to line_a + """ + # one of the 4 endpoints must be a closest point + pts = [ + (line_a.p1, line_b.p1), (line_a.p1, line_b.p2), + (line_a.p2, line_b.p1), (line_a.p2, line_b.p2) + ] + dists = [p1.distance_to_point(p2) for p1, p2 in pts] + # sort the closest points based on their distance + dists, i = zip(*sorted(zip(dists, range(len(pts))))) + return dists[0], pts[i[0]]
+ + + +
+[docs] +def closest_point2d_on_arc2d(point, arc): + """Get the closest Point2D on a Arc2D to the input point. + + Args: + point: A Point2D object. + arc: An Arc2D object along which the closest point will be determined. + + Returns: + Point2D for the closest point on arc to point. + """ + v = point - arc.c + v = v.normalize() * arc.r + if arc.is_circle: + return Point2D(arc.c.x + v.x, arc.c.y + v.y) + else: + a = Vector2D(1, 0).angle_counterclockwise(v) + if (not arc.is_inverted and arc.a1 < a < arc.a2) or \ + (arc.is_inverted and arc.a1 > a > arc.a2): + return Point2D(arc.c.x + v.x, arc.c.y + v.y) + else: + if arc.p1.distance_to_point(point) <= arc.p2.distance_to_point(point): + return arc.p1 + return arc.p2
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/intersection3d.html b/docs/_modules/ladybug_geometry/intersection3d.html new file mode 100644 index 00000000..6325cc28 --- /dev/null +++ b/docs/_modules/ladybug_geometry/intersection3d.html @@ -0,0 +1,1426 @@ + + + + + + + ladybug_geometry.intersection3d — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.intersection3d

+# coding=utf-8
+"""Utility functions for computing intersections between geometry in 3D space.
+
+Taken mostly from the euclid package available at
+https://pypi.org/project/euclid/
+"""
+from __future__ import division
+
+from .geometry3d.pointvector import Point3D
+
+import math
+
+
+
+[docs] +def intersect_line3d_plane(line_ray, plane): + """Get the intersection between a Ray3D/LineSegment3D and a Plane. + + Args: + line_ray: A LineSegment3D or Ray3D object. + plane: A Plane object to intersect. + + Returns: + Point3D of intersection if it exists. None if no intersection exists. + """ + d = plane.n.dot(line_ray.v) + if not d: # parallel + return None + u = (plane.k - plane.n.dot(line_ray.p)) / d + if not line_ray._u_in(u): # line or ray does not have its domain in the plane + return None + return Point3D(line_ray.p.x + u * line_ray.v.x, + line_ray.p.y + u * line_ray.v.y, + line_ray.p.z + u * line_ray.v.z)
+ + + +
+[docs] +def intersect_line3d_plane_infinite(line_ray, plane): + """Get the intersection between a Plane and Ray2D/LineSegment2D extended infinitely. + + Args: + line_ray: ALineSegment2D or Ray2D that will be extended infinitely + for intersection. + plane: A Plane object to intersect. + + Returns: + Point3D of intersection if it exists. None if no intersection exists. + """ + d = plane.n.dot(line_ray.v) + if not d: # parallel + return None + u = (plane.k - plane.n.dot(line_ray.p)) / d + return Point3D(line_ray.p.x + u * line_ray.v.x, + line_ray.p.y + u * line_ray.v.y, + line_ray.p.z + u * line_ray.v.z)
+ + + +
+[docs] +def intersect_plane_plane(plane_a, plane_b): + """Get the intersection between two Plane objects. + + Args: + plane_a: A Plane object. + plane_b: Another Plane object to intersect. + + Returns: + Two objects that define the intersection between two planes + + 1) A Point3D that lies along the intersection of the two planes. + 2) A Vector3D that describes the direction of the intersection. + + Will be None if no intersection exists (planes are parallel). + """ + n1_m = plane_a.n.magnitude_squared + n2_m = plane_b.n.magnitude_squared + n1d2 = plane_a.n.dot(plane_b.n) + det = n1_m * n2_m - n1d2 ** 2 + if det == 0: # parallel + return None + c1 = (plane_a.k * n2_m - plane_b.k * n1d2) / det + c2 = (plane_b.k * n1_m - plane_a.k * n1d2) / det + return Point3D(c1 * plane_a.n.x + c2 * plane_b.n.x, + c1 * plane_a.n.y + c2 * plane_b.n.y, + c1 * plane_a.n.z + c2 * plane_b.n.z), plane_a.n.cross(plane_b.n)
+ + + +
+[docs] +def closest_point3d_on_line3d(point, line_ray): + """Get the closest Point3D on a LineSegment3D or Ray3D to the input point. + + Args: + point: A Point3D object. + line_ray: A LineSegment3D or Ray3D object along which the closest point + will be determined. + + Returns: + Point3D for the closest point on line_ray to point. + """ + d = line_ray.v.magnitude_squared + if d == 0: # zero-length segment; just return the end point + return line_ray.p + u = ((point.x - line_ray.p.x) * line_ray.v.x + + (point.y - line_ray.p.y) * line_ray.v.y + + (point.z - line_ray.p.z) * line_ray.v.z) / d + if not line_ray._u_in(u): + u = max(min(u, 1.0), 0.0) + return Point3D(line_ray.p.x + u * line_ray.v.x, + line_ray.p.y + u * line_ray.v.y, + line_ray.p.z + u * line_ray.v.z)
+ + + +
+[docs] +def closest_point3d_on_line3d_infinite(point, line_ray): + """Get the closest Point3D on an infinite extension of a LineSegment3D or Ray3D. + + Args: + point: A Point3D object. + line_ray: A LineSegment3D or Ray3D object along which the closest point + will be determined. + + Returns: + Point3D for the closest point on the line_ray to the point. + """ + d = line_ray.v.magnitude_squared + if d == 0: # zero-length segment; just return the end point + return line_ray.p + u = ((point.x - line_ray.p.x) * line_ray.v.x + + (point.y - line_ray.p.y) * line_ray.v.y + + (point.z - line_ray.p.z) * line_ray.v.z) / d + return Point3D(line_ray.p.x + u * line_ray.v.x, + line_ray.p.y + u * line_ray.v.y, + line_ray.p.z + u * line_ray.v.z)
+ + + +
+[docs] +def closest_point3d_on_plane(point, plane): + """Get the closest Point3D on a Plane to the input point. + + Args: + point: A Point3D object. + plane: A Plane object in which the closest point will be determined. + + Returns: + Point3D for the closest point on the plane to point. + """ + n = plane.n + d = point.dot(plane.n) - plane.k + return Point3D(point.x - n.x * d, point.y - n.y * d, point.z - n.z * d)
+ + + +
+[docs] +def closest_point3d_between_line3d_plane(line_ray, plane): + """Get the two closest Point3D between a LineSegment3D/Ray3D and a Plane. + + Args: + line_ray: A LineSegment3D or Ray3D object along which the closest point + will be determined. + plane: A Plane object on which a closest point will be determined. + + Returns: + Two Point3D objects representing + + 1) The point on the line_ray that is closest to the plane. + 2) The point on the plane that is closest to the line_ray. + + Will be None if there is an intersection between line_ray and the plane + """ + d = plane.n.dot(line_ray.v) + if not d: # parallel, choose an endpoint + return line_ray.p, closest_point3d_on_plane(line_ray.p, plane) + u = (plane.k - plane.n.dot(line_ray.p)) / d + if not line_ray._u_in(u): # intersects out of range of L, choose nearest endpoint + u = max(min(u, 1.0), 0.0) + close_pt = Point3D(line_ray.p.x + u * line_ray.v.x, + line_ray.p.y + u * line_ray.v.y, + line_ray.p.z + u * line_ray.v.z) + return close_pt, closest_point3d_on_plane(close_pt, plane) + return None # intersection
+ + + +
+[docs] +def intersect_line3d_sphere(line_ray, sphere): + """Get the intersection between this Sphere object and a Ray2D/LineSegment2D. + + Args: + line_ray: A LineSegment3D or Ray3D for intersection. + sphere: A Sphere to intersect. + + Returns: + Two Point3D objects if a full intersection exists. + A Point3D if a point of tangency exists. + Will be None if no intersection exists. + + """ + L = line_ray + S = sphere + a = L.v.magnitude_squared + b = 2 * (L.v.x * (L.p.x - S.center.x) + + L.v.y * (L.p.y - S.center.y) + + L.v.z * (L.p.z - S.center.z)) + c = S.center.magnitude_squared + \ + L.p.magnitude_squared - \ + 2 * S.center.dot(L.p) - \ + S.radius ** 2 + det = b ** 2 - 4 * a * c + if det < 0: + return None + sq = math.sqrt(det) + u1 = (-b + sq) / (2 * a) + u2 = (-b - sq) / (2 * a) + if not L._u_in(u1): + u1 = max(min(u1, 1.0), 0.0) + if not L._u_in(u2): + u2 = max(min(u2, 1.0), 0.0) + p1 = Point3D(L.p.x + u1 * L.v.x, L.p.y + u1 * L.v.y, L.p.z + u1 * L.v.z) + if u1 == u2: + return p1 + else: + p2 = Point3D(L.p.x + u2 * L.v.x, L.p.y + u2 * L.v.y, L.p.z + u2 * L.v.z) + return p1, p2
+ + + +
+[docs] +def intersect_plane_sphere(plane, sphere): + """Get the intersection of a plane with this Sphere object + + Args: + plane: A Plane object. + sphere: A Sphere to intersect. + + Returns: + If a full intersection exists + + 1) A Point3D that represents the center of the intersection circle. + 2) A Vector3D that represents the normal of the intersection circle. + 3) A number that represents the radius of the intersection circle. + + A Point3D Object if a point of tangency exists. + None if no intersection exists. + """ + r = sphere.radius + pt_c = sphere.center + pt_o = plane.o + v_n = plane.n.normalize() + + # Resulting circle radius. Radius² = r² - [(c-p).n]² + d = (pt_o - pt_c).dot(v_n) + if abs(r) < abs(d): # No intersection if (r ** 2 - d ** 2) negative + return None + cut_r = math.sqrt(r ** 2 - d ** 2) + + # Intersection circle center point. Center_point = p - [(c-p).n]n + cut_center = pt_c + (d * v_n) + + return (cut_center, v_n, cut_r) if cut_r != 0 else cut_center
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/network.html b/docs/_modules/ladybug_geometry/network.html new file mode 100644 index 00000000..74d30cc8 --- /dev/null +++ b/docs/_modules/ladybug_geometry/network.html @@ -0,0 +1,2136 @@ + + + + + + + ladybug_geometry.network — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.network

+# coding=utf-8
+"""A 2D Directed Graph Network data structure used for splitting polygons.
+
+A directed graph (or digraph) is a graph made up of a set of vertices (aka. nodes)
+connected by directed edges, often into a network where each node can have
+multiple connections.
+
+This class is used in all operations where a polygon or face is split using
+line segments or polylines.
+
+The overall strategies used in this module are inspired by operations performed by
+NetworkX but were constructed from scratch without working from any particular
+module or class in the package More information on NetworkX can be found here:
+https://github.com/networkx/networkx
+"""
+from __future__ import division
+import math
+
+from ladybug_geometry.intersection2d import intersect_line_segment2d
+from ladybug_geometry.geometry2d import LineSegment2D
+
+
+
+[docs] +def coordinates_hash(point, tolerance): + """Convert XY coordinates of a Point2D into a string useful for hashing. + + Points that are co-located within the tolerance will receive the same string value + from this function, which helps convert line segments that contain duplicated + vertex references them into a singular network object where co-located vertices + are referenced only once. + + Args: + point: A Point2D object. + tolerance: floating point precision tolerance. + + Returns: + A string of rounded coordinates. + """ + # get the relative tolerance using a log function + try: + rtol = int(math.log10(tolerance)) * -1 + except ValueError: + rtol = 0 # the tol is equal to 1 (out of range for log) + # account for the fact that the tolerance may not be base 10 + base = int(tolerance * 10 ** (rtol + 1)) + if base == 10 or base == 0: # tolerance is base 10 (eg. 0.001) + base = 1 + else: # tolerance is not base 10 (eg. 0.003) + rtol += 1 + # avoid cases of signed zeros messing with the hash + z_tol = tolerance / 2 + x_val = 0.0 if abs(point.x) < z_tol else point.x + y_val = 0.0 if abs(point.y) < z_tol else point.y + # convert the coordinate values to a hash + return str(( + base * round(x_val / base, rtol), + base * round(y_val / base, rtol) + ))
+ + + +
+[docs] +class Node(object): + """A Node within in DirectedGraphNetwork, optionally connected to other Nodes. + + Args: + pt: A Point2D object for the node + key: String representation of the Point2D object which accounts for tolerance. + order: Integer for the order of the Node (based on directed graph propagation). + adj_lst: List of Node objects that are adjacent to this node. + exterior: Optional boolean to indicate if the Node is on the exterior of + the graph. If None, this value can be computed later based on the + position within the overall graph. + + Properties: + * pt + * key + * adj_lst + * exterior + * adj_count + """ + __slots__ = ('key', 'pt', '_order', 'adj_lst', 'exterior') + + def __init__(self, pt, key, order, adj_lst, exterior=None): + """Initialize Node.""" + self.pt = pt + self.key = key + self._order = order + self.adj_lst = adj_lst + self.exterior = exterior + + @property + def adj_count(self): + """Number of adjacent nodes""" + return len(self.adj_lst) + + def __hash__(self): + return hash(self.key) + + def __eq__(self, other): + return isinstance(other, Node) and \ + self.key == other.key + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return '{}: {}'.format(self._order, self.key)
+ + + +
+[docs] +class DirectedGraphNetwork(object): + """A 2D Directed Graph Network data structure used for splitting polygons. + + A directed graph (or digraph) is a graph made up of a set of vertices (aka. nodes) + connected by directed edges, often into a network where each node can have + multiple connections. This class contains for finding the shortest pathways + through the graph. It also helps differentiate interior from exterior parts + of the graph. Typically, interior pathways are bi-directional in the graph + while exterior pathways are uni-directional. + + Args: + tolerance: The tolerance used to determine point equivalence throughout + the graph. This is used for hashing points within the network. + + Properties: + * node_count: Integer for the number of nodes in graph. + * nodes: An iterable of nodes in graph. + * ordered_nodes: An iterable of nodes in graph in order they were added. + * connection_segments: List of LineSegment2D for the node connections. + * outer_root_node: A node for the outer root key. + * hole_root_nodes: A list of nodes for the hole root keys. + """ + __slots__ = ('_directed_graph', '_tolerance', 'outer_root_key', 'hole_root_keys') + + def __init__(self, tolerance): + """Initialize a PolygonDirectedGraph.""" + self._directed_graph = {} # will be used to hold the nodes of the network + # multiply tolerance by 2 to catch both positive and negative point equivalence + self._tolerance = tolerance * 2 + self.outer_root_key = None # will be set during network creation + self.hole_root_keys = [] # will be set during network creation + +
+[docs] + @classmethod + def from_point_array(cls, point_array, tolerance, loop=True): + """Create a DirectedGraphNetwork for a 1-dimensional array of points. + + Args: + point_array: Array of Point2D objects. + tolerance: The tolerance used to determine point equivalence throughout + the graph. This is used for hashing points within the network. + loop: Optional boolean, which will ensure that the input point_array + is connected into a loop. (Default: True). + """ + ext = True if loop else None + dg = cls(tolerance) + for i in range(len(point_array) - 1): + k = dg.add_node(point_array[i], [point_array[i + 1]], exterior=ext) + if i == 0: + dg.outer_root_key = k + if loop: + dg.add_node(point_array[-1], [point_array[0]], exterior=ext) + return dg
+ + +
+[docs] + @classmethod + def from_polygon(cls, polygon, tolerance): + """Create a DirectedGraphNetwork from a polygon. + + Args: + polygon: A Polygon2D object. + tolerance: The tolerance used to determine point equivalence throughout + the graph. This is used for hashing points within the network. + + Returns: + A PolygonDirectedGraph object that represents the polygon. The edges + are uni-directional and counterclockwise. The nodes have the exterior + property set to True. + """ + # ensure the boundary and holes are oriented correctly for the graph + if polygon.is_clockwise: + polygon = polygon.reverse() + return cls.from_point_array(polygon.vertices, tolerance, loop=True)
+ + +
+[docs] + @classmethod + def from_shape_with_holes(cls, boundary, holes, tolerance): + """Create a DirectedGraphNetwork for a shape with holes. + + Args: + boundary: A Polygon2D for the boundary around the shape. + holes: An optional list of Polygon2D for the holes within the shape. + If None, it will be assumed that no holes exist in the shape. + tolerance: The tolerance used to determine point equivalence throughout + the graph. This is used for hashing points within the network. + + Returns: + A PolygonDirectedGraph object that represents the boundary with holes. + The edges (including the boundary and holes) are uni-directional with + the outer boundary being counterclockwise and the holes being clockwise. + In other words, the fill of the shape is always to the left of each edge. + The nodes have the exterior property set to True. + """ + # ensure the boundary and holes are oriented correctly for the graph + if boundary.is_clockwise: + boundary = boundary.reverse() + loops = [boundary.vertices] + if holes is not None: + for hole in holes: + if hole.is_clockwise: + loops.append(hole.vertices) + else: + loops.append(hole.reverse().vertices) + + # make the directed graph and add the nodes for the boundary + holes + dg = cls(tolerance=tolerance) + for loop_count, vertices in enumerate(loops): + for j in range(len(vertices) - 1): + curr_v = vertices[j] + next_v = vertices[j + 1] + k = dg.add_node(curr_v, [next_v], exterior=True) + if j == 0: + if loop_count == 0: + dg.outer_root_key = k + else: + dg.hole_root_keys.append(k) + dg.add_node(vertices[-1], [vertices[0]], exterior=True) # close loop + return dg
+ + +
+[docs] + @classmethod + def from_shape_to_split(cls, boundary, holes, split_segments, tolerance): + """Get a DirectedGraphNetwork for a shape to be split with segments. + + The shape is composed of a boundary with optional holes and the + split_segments are an array of any line segments to be used to + split the shape. + + Args: + boundary: A Polygon2D for the boundary around the shape. + holes: An optional list of Polygon2D for the holes within the shape. + If None, it will be assumed that no holes exist in the shape. + split_segments: An array of LineSegment2D to be used to split the shape. + tolerance: The tolerance used to determine point equivalence throughout + the graph. This is used for hashing points within the network. + + Returns: + A PolygonDirectedGraph object that represents the network formed by + the boundary, holes, and split segments. Portions of the split_segments + that are outside of the boundary are excluded from the graph. All interior + connections between the split_segments in the graph are bi-directional. + The exterior edges (including the boundary and holes) are uni-directional + with the outer boundary being counterclockwise and the holes being + clockwise. In other words, the fill of the shape is always to the left + of each exterior edge. The nodes at the boundary and the holes have + the exterior property set to True. + """ + # first split the boundary and holes with the split_segments + boundary = boundary.remove_colinear_vertices(tolerance) + bound_sgs = cls._intersect_segments(boundary.segments, split_segments, tolerance) + bound_pts = [seg.p1 for seg in bound_sgs] + split_boundary = boundary.__class__(bound_pts) + split_holes = None + if holes is not None: + split_holes = [] + for hole in holes: + hole = hole.remove_colinear_vertices(tolerance) + hole_sgs = cls._intersect_segments( + hole.segments, split_segments, tolerance) + hole_pts = [seg.p1 for seg in hole_sgs] + split_holes.append(boundary.__class__(hole_pts)) + + # make the directed graph for the boundary + holes + dg = cls.from_shape_with_holes(split_boundary, split_holes, tolerance) + + # process the split_segments for intersection + add_segs = list(boundary.segments) + if holes is not None: + for hole in holes: + add_segs.extend(hole.segments) + split_seg = cls._intersect_segments(split_segments, add_segs, tolerance) + split_seg = cls._remove_segments_outside_boundary(split_seg, boundary, tolerance) + if len(split_seg) == 0: # none of the segments are inside the shape + return dg + + # add the intersection segments to the graph + for seg in split_seg: # add a bidirectional edge to represent interior edges + dg.add_node(seg.p2, [seg.p1], exterior=False) + dg.add_node(seg.p1, [seg.p2], exterior=False) + return dg
+ + + @property + def node_count(self): + return len(self.nodes) + + @property + def nodes(self): + """Get an iterable of pt nodes""" + return self._directed_graph.values() + + @property + def ordered_nodes(self): + """Get an iterable of pt nodes in order of addition""" + nodes = list(self.nodes) + nodes.sort(key=lambda v: v._order) + return nodes + + @property + def outer_root_node(self): + """Get the node of the outer boundary root.""" + return self.node(self.outer_root_key) + + @property + def hole_root_nodes(self): + """Get a list of nodes for the roots of the holes.""" + return [self.node(hole_key) for hole_key in self.hole_root_keys] + + @property + def connection_segments(self): + """Get a list of LineSegment2D for the node connections in the graph.""" + traversed = set() + connections = [] + for node in self.nodes: + for conn_node in node.adj_lst: + if (conn_node.key, node.key) not in traversed: + conn_seg = LineSegment2D.from_end_points(node.pt, conn_node.pt) + connections.append(conn_seg) + traversed.add((node.key, conn_node.key)) + return connections + +
+[docs] + def node(self, key): + """Retrieves the node based on passed value. + + Args: + val: The key for a node in the directed graph. + + Returns: + The node for the passed key. + """ + try: + return self._directed_graph[key] + except KeyError: + return None # broken connection
+ + +
+[docs] + def add_adj(self, node, adj_val_lst): + """Adds nodes to node.adj_lst. + + This method will ensure no repetitions will occur in adj_lst. + + Args: + node: Node to add adjacencies to. + adj_val_lst: List of Point2D objects to add as adjacent nodes. + """ + adj_keys = {n.key: None for n in node.adj_lst} + adj_keys[node.key] = None + for adj_val in adj_val_lst: + adj_key = coordinates_hash(adj_val, self._tolerance) + if adj_key in adj_keys: + continue + + self._add_node(adj_key, adj_val, exterior=None) + adj_keys[adj_key] = None + node.adj_lst.append(self.node(adj_key))
+ + +
+[docs] + def remove_adj(self, node, adj_key_lst): + """Removes nodes in node.adj_lst. + + Args: + node: Node to remove adjacencies to. + adj_val_lst: List of adjacency keys to remove as adjacent nodes. + """ + node.adj_lst = [n for n in node.adj_lst if n.key not in set(adj_key_lst)]
+ + +
+[docs] + def add_node(self, val, adj_lst, exterior=None): + """Add a node into the PolygonDirectedGraph. + + This method consumes a Point2D, computes its key value, and adds it in the + graph if it doesn't exist. If it does exist it appends adj_lst to existing pt. + + Args: + val: A Point2D object. + adj_lst: A list of Point2D objects adjacent to the node. + exterior: Optional boolean for whether the Node is exterior. + + Returns: + The hashed key from the existing or new node. + """ + key = coordinates_hash(val, self._tolerance) # get key + self._add_node(key, val, exterior) # get node if it exists + node = self._directed_graph[key] + self.add_adj(node, adj_lst) # add the adj_lst to dg + # if the exterior boolean was passed, change the node attribute + if exterior is not None: + node.exterior = exterior + return node.key
+ + + def _add_node(self, key, val, exterior=None): + """Helper function for add_node. + + If key doesn't currently exist in the graph, it is added with a new key. + """ + if key not in self._directed_graph: + self._directed_graph[key] = Node(val, key, self.node_count, [], exterior) + return self._directed_graph[key] + +
+[docs] + def insert_node(self, base_node, new_val, next_node, exterior=None): + """Insert node in the middle of an edge defined by node and next_node. + + Args: + base_node: Node object to the left. + new_val: A Point2D object for the new node in the middle. + next_node: Node object to the right. + exterior: Optional boolean for exterior attribute. + + Returns: + key of new_val node. + """ + # add new_val as a node, with next_node as an adjacency + new_key = self.add_node(new_val, [next_node.pt], exterior=exterior) + # update parent by adding new adjacency, and removing old adjacency + self.add_adj(base_node, [self.node(new_key).pt]) + + # catch the edge case where the new point is coincident to parent or next_point. + # this occurs when intersection passes through a corner. + if (new_key == next_node.key) or (new_key == base_node.key): + return new_key + self.remove_adj(base_node, [next_node.key]) + return new_key
+ + +
+[docs] + def node_exists(self, key): + """Check if a node is in the graph. True if node in directed graph else False.""" + return key in self._directed_graph
+ + +
+[docs] + def pt_exists(self, pt): + """True if a point (as Point2D) in directed graph exists as node else False. + """ + return self.node_exists(coordinates_hash(pt, self._tolerance))
+ + +
+[docs] + def polygon_exists(self, polygon): + """Check if a polygon is in the directed graph. + + Args: + polygons: A Polygon2D object. + + Return: + True if exists, else False. + """ + vertices_loop = list(polygon.vertices) + vertices_loop = vertices_loop + [vertices_loop[0]] + + for i in range(len(vertices_loop) - 1): + pt1 = vertices_loop[i] + pt2 = vertices_loop[i + 1] + + if not self.pt_exists(pt1): + return False + + node1 = self.node(coordinates_hash(pt1, self._tolerance)) + node2 = self.node(coordinates_hash(pt2, self._tolerance)) + if node2.key in [n.key for n in node1.adj_lst]: + return False + + return True
+ + +
+[docs] + def adj_matrix(self): + """Gets an adjacency matrix of the directed graph where: + + * 1 = adjacency from row node to col node. + * 0 = no adjacency. + + Returns: + N x N square matrix where N is number of nodes. + """ + nodes = self.ordered_nodes + # initialize a mtx with no adjacencies + amtx = [[0 for i in range(self.node_count)] + for j in range(self.node_count)] + + for i in range(self.node_count): + adj_indices = [adj._order for adj in nodes[i].adj_lst] + for adj_idx in adj_indices: + amtx[i][adj_idx] = 1 + + return amtx
+ + +
+[docs] + def adj_matrix_labels(self): + """Returns a dictionary where label key corresponds to index in adj_matrix + and value is node key""" + return {i: node.key for i, node in enumerate(self.ordered_nodes)}
+ + +
+[docs] + def min_cycle(self, base_node, goal_node, ccw_only=False): + """Identify the shortest interior cycle between two exterior nodes. + + Args: + base_node: The first exterior node of the edge. + goal_node: The end exterior node of the cycle that, together with + the base_node, constitutes an exterior edge. + ccw_only: A boolean to note whether the search should be limited + to the counter-clockwise direction only. (Default: False). + + Returns: + A list of nodes that form a polygon if the cycle exists, else None. + """ + # set up a queue for exploring the graph + explored = [] + queue = [[base_node]] + orig_dir = base_node.pt - goal_node.pt \ + if base_node.key != goal_node.key else None + # loop to traverse the graph with the help of the queue + while queue: + path = queue.pop(0) + node = path[-1] + # make sure that the current node has not been visited + if node not in explored: + prev_dir = node.pt - path[-2].pt if len(path) > 1 else orig_dir + # iterate over the neighbors to determine relevant nodes + rel_neighbors, rel_angles = [], [] + last_resort_neighbors, last_resort_angles = [], [] + for neighbor in node.adj_lst: + if neighbor == goal_node: # the shortest path was found! + path.append(goal_node) + return path + edge_dir = neighbor.pt - node.pt + cw_angle = prev_dir.angle_clockwise(edge_dir * -1) \ + if prev_dir is not None else math.pi + if 1e-5 < cw_angle < (2 * math.pi) - 1e-5: + rel_neighbors.append(neighbor) + rel_angles.append(cw_angle) + else: # try to avoid back-tracking along the search + last_resort_neighbors.append(neighbor) + last_resort_angles.append(cw_angle) + if len(rel_neighbors) == 0: # back tracking is the only option + rel_neighbors = last_resort_neighbors + rel_angles = last_resort_angles + # sort the neighbors by clockwise angle + if len(rel_neighbors) > 1: + rel_neighbors = [n for _, n in sorted(zip(rel_angles, rel_neighbors), + key=lambda pair: pair[0])] + # add the relevant neighbors to the path and the queue + if ccw_only: + new_path = list(path) + new_path.append(rel_neighbors[0]) + queue.append(new_path) + else: # add all neighbors to the search + for neighbor in rel_neighbors: + new_path = list(path) + new_path.append(neighbor) + queue.append(new_path) + explored.append(node) + # if we reached the end of the queue, then no path was found + return None
+ + +
+[docs] + def all_min_cycles(self): + """Get a list of lists where each sub-list is a minimum cycle of Nodes. + + The combination of all min cycles should account for the full area of + the input shape if the DirectedGraphNetwork was made using any of the + class methods that work from polygons. If the DirectedGraphNetwork was made + using the from_shape_to_split method, the resulting cycles here represent + the input shape split with the split_segments. + """ + # first, figure out how many loops each node should be a part of + node_cycle_counts = {} + for node in self.nodes: + node_cycle_counts[node.key] = len(node.adj_lst) + + # loop through the nodes until all min cycles have been identified + all_cycles = [] + iter_count = 0 + max_iter = len(self.nodes) + remaining_nodes = self.ordered_nodes + explored_nodes = set() + + while len(remaining_nodes) > 1 and iter_count < max_iter: + # try to identify two connected nodes which we can use to build a cycle + cycle_root = remaining_nodes[0] + next_node = cycle_root # if we can't find a connected node, connect to self + ext_cycle = False + if cycle_root.exterior: # exterior cycles tend to have clear connections + next_node = DirectedGraphNetwork.next_exterior_node(cycle_root) + if next_node is not None: + ext_cycle = True + else: + next_node = cycle_root + if not ext_cycle: # see if we can connect it to another incomplete node + for _next_node in cycle_root.adj_lst: + if node_cycle_counts[_next_node.key] != 0: + next_node = _next_node + ext_cycle = True + break + + # find the minimum cycle by searching counter-clockwise + min_cycle = self.min_cycle(next_node, cycle_root, True) + + # if we found a minimum cycle, evaluate its validity by node connections + if min_cycle is not None and len(min_cycle) >= 3: + if not ext_cycle: + min_cycle.pop(-1) # take out the last duplicated node + is_valid_cycle = True + for node in min_cycle: + if node_cycle_counts[node.key] - 1 < 0: # we are re-traversing + is_valid_cycle = False # not a valid cycle + + # add the valid cycle to the list to be returned + if is_valid_cycle: + for node in min_cycle: + node_cycle_counts[node.key] = node_cycle_counts[node.key] - 1 + if node_cycle_counts[node.key] == 0: # all cycles for node found + for i, r_node in enumerate(remaining_nodes): + if r_node.key == node.key: + remaining_nodes.pop(i) + break + all_cycles.append(min_cycle) + for node in min_cycle: + explored_nodes.add(node.key) + + # reorder the remaining nodes so unexplored nodes get prioritized + if len(remaining_nodes) != 0: + for j, node in enumerate(remaining_nodes): + if node.key not in explored_nodes: + break + remaining_nodes.insert(0, remaining_nodes.pop(j)) + iter_count += 1 + + # if we wer not able to address all nodes, see if they are all in the same loop + if len(remaining_nodes) >= 3: + current_node = remaining_nodes.pop(0) + current_node_adj = [node.key for node in node.adj_lst] + last_cycle = [current_node] + iter_count, max_iter = 0, len(remaining_nodes) + while len(remaining_nodes) > 0 and iter_count < max_iter: + for k, node in enumerate(remaining_nodes): + if node.key in current_node_adj: + current_node = remaining_nodes.pop(k) + current_node_adj = [node.key for node in node.adj_lst] + last_cycle.append(current_node) + break + iter_count += 1 + if len(last_cycle) > 2: + all_cycles.append(last_cycle) + + return all_cycles
+ + +
+[docs] + def exterior_cycle(self, cycle_root): + """Compute exterior boundary from a given node. + + This method assumes that exterior edges are naked (unidirectional) and + interior edges are bidirectional. + + Args: + cycle_root: Starting Node in exterior cycle. + + Returns: + List of nodes on exterior if a cycle exists, else None. + """ + # Get the first exterior edge + curr_node = cycle_root + next_node = DirectedGraphNetwork.next_exterior_node(curr_node) + if not next_node: + return None + + # loop through the cycle until we get it all or run out of points + max_iter = self.node_count + 1 # maximum length a cycle can be + ext_cycle = [curr_node] + iter_count = 0 + while next_node.key != cycle_root.key: + ext_cycle.append(next_node) + next_node = DirectedGraphNetwork.next_exterior_node(next_node) + if not next_node: + return None # we have hit a dead end in the cycle + iter_count += 1 + if iter_count > max_iter: + break # we have gotten stuck in a loop + + return ext_cycle
+ + +
+[docs] + def exterior_cycles(self): + """Get a list of lists where each sub-list is an exterior cycle of Nodes. + + Exterior cycles refer to the cycles of both the boundary and the holes + of the DirectedGraphNetwork was created using the from_shape_to_split + class method. + """ + exterior_poly_lst = [] # list to store cycles + explored_nodes = set() # set to note explored exterior nodes + max_iter = self.node_count + 1 # maximum length a cycle can be + + # loop through all of the nodes of the graph and find cycles + for root_node in self.ordered_nodes: + # make a note that the current node has been explored + explored_nodes.add(root_node.key) # mark the node as explored + # get next exterior adjacent node and check that it's valid + next_node = self.next_exterior_node(root_node) # mark the node as explored + is_valid = (next_node is not None) and (next_node.key not in explored_nodes) + if not is_valid: + continue + # make a note that the next node has been explored + explored_nodes.add(next_node.key) + + # traverse the loop of points until we get back to start or hit a dead end + exterior_poly = [root_node] + prev_node = root_node + iter_count = 0 + while next_node.key != root_node.key: + exterior_poly.append(next_node) + explored_nodes.add(next_node.key) # mark the node as explored + follow_node = self.next_exterior_node_no_backtrack( + next_node, prev_node, explored_nodes) + prev_node = next_node # set as the previous node for the next step + next_node = follow_node + if next_node is None: + break # we have hit a dead end in the cycle + iter_count += 1 + if iter_count > max_iter: + print('Extraction of core polygons hit an endless loop.') + break # we have gotten stuck in a loop + exterior_poly_lst.append(exterior_poly) + + # return all of the exterior loops that were found + return exterior_poly_lst
+ + +
+[docs] + @staticmethod + def next_exterior_node_no_backtrack(node, previous_node, explored_nodes): + """Get the next exterior node adjacent to the input node. + + This method is similar to the next_exterior_node method but it includes + extra checks to handle intersections with 3 or more segments in the + graph exterior cycles. In these cases a set of previously explored_nodes + is used to ensure that no back-tracking happens over the search of the + network, which can lead to infinite looping through the graph. Furthermore, + the previous_node is used to select the pathway with the smallest angle + difference with the previous direction. This leads the result towards + minimal polygons with fewer self-intersecting loops. + + Args: + node: A Node object for which the next node will be returned. + previous_node: A Node object for the node that came before + the current one in the loop. This will be used in the event that + multiple exterior nodes are found connecting to the input node. + In this case, the exterior node with the smallest angle difference + with the previous direction will be returned. This leads the + result towards minimal polygons and away from self-intersecting + exterior loops like a bowtie. + + Returns: + Next node that defines exterior edge, or None if all adjacencies are + bidirectional. + """ + # loop through the all adjacent nodes and determine if they are exterior + next_nodes = [] + for _next_node in node.adj_lst: + if _next_node.exterior: # user has labeled it as exterior; we're done! + return _next_node + elif _next_node.exterior is None: # don't know if it's interior or exterior + # if user-assigned attribute isn't defined, check bi-directionality + if not DirectedGraphNetwork.is_edge_bidirect(node, _next_node): + next_nodes.append(_next_node) + + # evaluate whether there is one obvious choice for the next node + if len(next_nodes) <= 1: + return next_nodes[0] if len(next_nodes) == 1 else None + next_nodes = [nn for nn in next_nodes if nn.key not in explored_nodes] + if len(next_nodes) <= 1: + return next_nodes[0] if len(next_nodes) == 1 else None + + # if we have multiple exterior nodes, use the previous node to find the best one + prev_dir = previous_node.pt - node.pt # yields a vector + next_angles = [] + for next_node in next_nodes: + edge_dir = next_node.pt - node.pt # yields a vector + next_angles.append(prev_dir.angle(edge_dir * -1)) + sorted_nodes = [n for _, n in sorted(zip(next_angles, next_nodes), + key=lambda pair: pair[0])] + return sorted_nodes[0] # return the node making the smallest angle
+ + +
+[docs] + @staticmethod + def next_exterior_node(node): + """Get the next exterior node adjacent to consumed node. + + If there are adjacent nodes that are labeled as exterior, with True or + False defining the Node.exterior property, the first of such nodes in + the adjacency list will be returned as the next one. Otherwise, the + bi-directionality will be used to determine whether the next node is + exterior. + + Args: + node: A Node object for which the next node will be returned. + + Returns: + Next node that defines exterior edge, or None if all adjacencies are + bidirectional. + """ + # loop through the adjacency and find an exterior node + for _next_node in node.adj_lst: + if _next_node.exterior: # user has labeled it as exterior; we're done! + return _next_node + elif _next_node.exterior is None: # don't know if it's interior or exterior + # if user-assigned attribute isn't defined, check bi-directionality + if not DirectedGraphNetwork.is_edge_bidirect(node, _next_node): + return _next_node + return None
+ + +
+[docs] + @staticmethod + def is_edge_bidirect(node1, node2): + """Are two nodes bidirectional. + + Args: + node1: Node object + node2: Node object + + Returns: + True if node1 and node2 are in each other's adjacency list, + else False. + """ + return node1.key in (n.key for n in node2.adj_lst) and \ + node2.key in (n.key for n in node1.adj_lst)
+ + + @staticmethod + def _intersect_segments(segments, additional_segments, tolerance): + """Intersect a list of LineSegment2D and split them. + + Args: + segments: A list of LineSegment2D for the segments to be split/intersected. + additional_segments: A list of additional LineSegment2Ds, which will be + used to split the input segments but will not be included in the + output themselves. + tolerance: The tolerance at which the intersection will be computed. + + Returns: + A list of LineSegment2D for the input segments split through + self-intersection and intersection with the additional_segments. + """ + # make sure that we are working with lists + if not isinstance(segments, list): + segments = list(segments) + if not isinstance(additional_segments, list): + additional_segments = list(additional_segments) + + # extend segments a little to ensure intersections happen + under_tol = tolerance * 0.99 + ext_segments = [] + for seg in segments + additional_segments: + m_v = seg.v.normalize() * under_tol + ext_seg = LineSegment2D.from_end_points(seg.p1.move(-m_v), seg.p2.move(m_v)) + ext_segments.append(ext_seg) + + # compute all of the intersection points across the segments + intersect_pts = [[] for _ in segments] + for i, seg in enumerate(segments): + try: + for other_seg in ext_segments[:i] + ext_segments[i + 1:]: + int_pt = intersect_line_segment2d(seg, other_seg) + if int_pt is None or int_pt.is_equivalent(seg.p1, tolerance) or \ + int_pt.is_equivalent(seg.p2, tolerance): + continue + # we have found an intersection point where segments should be split + intersect_pts[i].append(int_pt) + except IndexError: + pass # we have reached the end of the list + + # loop through the segments and split them at the intersection points + split_segments = [] + for seg, split_pts in zip(segments, intersect_pts): + if len(split_pts) == 0: + split_segments.append(seg) + elif len(split_pts) == 1: # split the segment in two + int_pt = split_pts[0] + split_segments.append(LineSegment2D.from_end_points(seg.p1, int_pt)) + split_segments.append(LineSegment2D.from_end_points(int_pt, seg.p2)) + else: # sort the points along the segment to split it + pt_dists = [seg.p1.distance_to_point(ipt) for ipt in split_pts] + sort_obj = sorted(zip(pt_dists, split_pts), key=lambda pair: pair[0]) + sort_pts = [x for _, x in sort_obj] + sort_pts.append(seg.p2) + pr_pt = seg.p1 + for s_pt in sort_pts: + if not pr_pt.is_equivalent(s_pt, tolerance): + split_segments.append(LineSegment2D.from_end_points(pr_pt, s_pt)) + pr_pt = s_pt + + return split_segments + + @staticmethod + def _remove_segments_outside_boundary(segments, boundary, tolerance): + """Remove LineSegment2D that are outside the boundary of the parent shape. + + This can be used to clean up the result after intersection of segments. + + Args: + segments: A list of LineSegment2D to be filtered for whether they are + inside the boundary. + boundary: A Polygon2D for the boundary of the shape. Segments that lie + outside of this boundary beyond the tolerance will be removed from + the result. + tolerance: The tolerance for distinguishing whether skeleton points lie + outside the boundary. + + Returns: + A list of LineSegment2D objects with segments removed that outside + of the boundary. + """ + clean_segments = [] + for seg in segments: + p1, p2 = seg.p1, seg.p2 + if boundary.point_relationship(p2, tolerance) >= 0 and \ + boundary.point_relationship(p1, tolerance) >= 0: + clean_segments.append(seg) + return clean_segments + + def __repr__(self): + """Represent PolygonDirectedGraph.""" + s = '' + for n in self.ordered_nodes: + s += '{}, [{}]\n'.format( + n.pt.to_array(), + ', '.join([str(_n.pt.to_array()) for _n in n.adj_lst])) + return s
+ +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/ladybug_geometry/triangulation.html b/docs/_modules/ladybug_geometry/triangulation.html new file mode 100644 index 00000000..b20f014e --- /dev/null +++ b/docs/_modules/ladybug_geometry/triangulation.html @@ -0,0 +1,1864 @@ + + + + + + + ladybug_geometry.triangulation — ladybug geometry documentation + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +

Source code for ladybug_geometry.triangulation

+# coding=utf-8
+"""Core triangulation functions used by various geometry modules.
+
+The functions here are derived from the earcut-python library available at
+https://github.com/joshuaskelly/earcut-python
+
+The earcut-python library is, itself, a pure Python port of the earcut JavaScript
+triangulation library maintained by Mapbox. The original project can be found at
+https://github.com/mapbox/earcut
+
+The version here is based off of the JavaScript earcut 2.1.1 release, and is
+functionally identical.
+"""
+from __future__ import division
+
+
+
+[docs] +def earcut(data, hole_indices=None, dim=2): + """Triangulate a list of vertices that make up a shape, either with or without holes. + + Args: + data: A flat array of vertex coordinates like [x0,y0, x1,y1, x2,y2, ...]. + hole_indices: A flat array of the starting indices for each hole. For example, + [5, 8] for a 12-vertex input would mean one hole with vertices 5-7 + and another with 8-11. If a single vertex is passed as as a hole, + Earcut treats it as a Steiner point. If None, no holes will be assumed + for the shape. (Default: None). + dim: An integer for the number of coordinates per vertex in the input + array. For example, 3 means each vertex exists in 3D space with + XX, Y, Z coordinates. (Default: 2 for 2D coordinates). + """ + dim = dim or 2 + hasHoles = hole_indices and len(hole_indices) + outerLen = hole_indices[0] * dim if hasHoles else len(data) + outerNode = _linked_list(data, 0, outerLen, dim, True) + triangles = [] + + if not outerNode: + return triangles + + minX = None + minY = None + maxX = None + maxY = None + x = None + y = None + size = None + + if hasHoles: + outerNode = _eliminate_holes(data, hole_indices, outerNode, dim) + + # if the shape is not too simple, we'll use z-order curve hash later + if (len(data) > 80 * dim): # calculate polygon bbox + minX = maxX = data[0] + minY = maxY = data[1] + + for i in range(dim, outerLen, dim): + x = data[i] + y = data[i + 1] + if x < minX: + minX = x + if y < minY: + minY = y + if x > maxX: + maxX = x + if y > maxY: + maxY = y + + # minX, minY and size are later used to transform coords into integers + # integers are used for z-order calculation + size = max(maxX - minX, maxY - minY) + + _earcut_linked(outerNode, triangles, dim, minX, minY, size) + + return triangles
+ + + +def _linked_list(data, start, end, dim, clockwise): + """Create a circular doubly linked list from polygon points. + + Points will be in the specified winding order. + """ + i = None + last = None + + if (clockwise == (_signed_area(data, start, end, dim) > 0)): + for i in range(start, end, dim): + last = _insert_node(i, data[i], data[i + 1], last) + + else: + for i in reversed(range(start, end, dim)): + last = _insert_node(i, data[i], data[i + 1], last) + + if (last and _equals(last, last.next)): + _remove_node(last) + last = last.next + + return last + + +def _signed_area(data, start, end, dim): + """Get the signed area from a list of coordinate data.""" + sum = 0 + j = end - dim + + for i in range(start, end, dim): + sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]) + j = i + + return sum + + +def _filter_points(start, end=None): + """Eliminate colinear or duplicate points.""" + if not start: + return start + if not end: + end = start + + p = start + again = True + + while again or p != end: + again = False + + if (not p.steiner and (_equals(p, p.next) or _area(p.prev, p, p.next) == 0)): + _remove_node(p) + p = end = p.prev + if (p == p.next): + return None + + again = True + + else: + p = p.next + + return end + + +def _earcut_linked(ear, triangles, dim, minX, minY, size, _pass=None): + """Main ear slicing loop which triangulates a polygon (given as a linked list).""" + if not ear: + return + + # interlink polygon nodes in z-order + if not _pass and size: + _index_curve(ear, minX, minY, size) + + stop = ear + prev = None + next = None + + # iterate through ears, slicing them one by one + while ear.prev != ear.next: + prev = ear.prev + next = ear.next + + if _is_ear_hashed(ear, minX, minY, size) if size else _is_ear(ear): + # cut off the triangle + triangles.append(prev.i // dim) + triangles.append(ear.i // dim) + triangles.append(next.i // dim) + + _remove_node(ear) + + # skipping the next vertex leads to less sliver triangles + ear = next.next + stop = next.next + + continue + + ear = next + + # if we looped through the whole remaining polygon and can't find any more ears + if ear == stop: + # try filtering points and slicing again + if not _pass: + _earcut_linked(_filter_points(ear), triangles, dim, minX, minY, size, 1) + + # if this didn't work, try curing all small self-intersections locally + elif _pass == 1: + ear = _cure_local_intersections(ear, triangles, dim) + _earcut_linked(ear, triangles, dim, minX, minY, size, 2) + + # as a last resort, try splitting the remaining polygon into two + elif _pass == 2: + _split_earcut(ear, triangles, dim, minX, minY, size) + + break + + +def _is_ear(ear): + """Check whether a polygon node forms a valid ear with adjacent nodes.""" + a = ear.prev + b = ear + c = ear.next + + if _area(a, b, c) >= 0: + return False # reflex, can't be an ear + + # now make sure we don't have other points inside the potential ear + p = ear.next.next + + while p != ear.prev: + if _point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) and \ + _area(p.prev, p, p.next) >= 0: + return False + p = p.next + + return True + + +def _is_ear_hashed(ear, minX, minY, size): + """Check whether a polygon node forms a valid ear using hashes.""" + a = ear.prev + b = ear + c = ear.next + + if _area(a, b, c) >= 0: + return False # reflex, can't be an ear + + # triangle bbox; min & max are calculated like this for speed + minTX = (a.x if a.x < c.x else c.x) if a.x < b.x else (b.x if b.x < c.x else c.x) + minTY = (a.y if a.y < c.y else c.y) if a.y < b.y else (b.y if b.y < c.y else c.y) + maxTX = (a.x if a.x > c.x else c.x) if a.x > b.x else (b.x if b.x > c.x else c.x) + maxTY = (a.y if a.y > c.y else c.y) if a.y > b.y else (b.y if b.y > c.y else c.y) + + # z-order range for the current triangle bbox; + minZ = _z_order(minTX, minTY, minX, minY, size) + maxZ = _z_order(maxTX, maxTY, minX, minY, size) + + # first look for points inside the triangle in increasing z-order + p = ear.nextZ + + while p and p.z <= maxZ: + if p != ear.prev and p != ear.next and \ + _point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) and \ + _area(p.prev, p, p.next) >= 0: + return False + p = p.nextZ + + # then look for points in decreasing z-order + p = ear.prevZ + + while p and p.z >= minZ: + if p != ear.prev and p != ear.next and \ + _point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) and \ + _area(p.prev, p, p.next) >= 0: + return False + p = p.prevZ + + return True + + +def _cure_local_intersections(start, triangles, dim): + """Go through all polygon nodes and cure small local self-intersections.""" + do = True + p = start + + while do or p != start: + do = False + + a = p.prev + b = p.next.next + + if not _equals(a, b) and _intersects(a, p, p.next, b) and \ + _locally_inside(a, b) and _locally_inside(b, a): + triangles.append(a.i // dim) + triangles.append(p.i // dim) + triangles.append(b.i // dim) + + # remove two nodes involved + _remove_node(p) + _remove_node(p.next) + + p = start = b + + p = p.next + + return p + + +def _split_earcut(start, triangles, dim, minX, minY, size): + """try splitting polygon into two and triangulate them independently.""" + # look for a valid diagonal that divides the polygon into two + do = True + a = start + + while do or a != start: + do = False + b = a.next.next + + while b != a.prev: + if a.i != b.i and _is_valid_diagonal(a, b): + # split the polygon in two by the diagonal + c = _split_polygon(a, b) + + # filter colinear points around the cuts + a = _filter_points(a, a.next) + c = _filter_points(c, c.next) + + # run earcut on each half + _earcut_linked(a, triangles, dim, minX, minY, size) + _earcut_linked(c, triangles, dim, minX, minY, size) + return + + b = b.next + + a = a.next + + +def _eliminate_holes(data, hole_indices, outerNode, dim): + """Link holes into the outer loop, producing a single-ring polygon without holes.""" + queue = [] + i = None + _len = len(hole_indices) + start = None + end = None + _list = None + + for i in range(len(hole_indices)): + start = hole_indices[i] * dim + end = hole_indices[i + 1] * dim if i < _len - 1 else len(data) + _list = _linked_list(data, start, end, dim, False) + + if (_list == _list.next): + _list.steiner = True + + queue.append(_get_leftmost(_list)) + + queue = sorted(queue, key=lambda i: i.x) + + # process holes from left to right + for i in range(len(queue)): + _eliminate_hole(queue[i], outerNode) + outerNode = _filter_points(outerNode, outerNode.next) + + return outerNode + + +def _eliminate_hole(hole, outerNode): + """Find a bridge between vertices that connects hole with an outer ring. + + Return a shape with the hole linked into it.""" + outerNode = _find_hole_bridge(hole, outerNode) + if outerNode: + b = _split_polygon(outerNode, hole) + _filter_points(b, b.next) + + +def _find_hole_bridge(hole, outerNode): + """David Eberly's algorithm for finding a bridge between hole and outer polygon.""" + do = True + p = outerNode + hx = hole.x + hy = hole.y + qx = float('-inf') + m = None + + # find a segment intersected by a ray from the hole's leftmost point to the left; + # segment's endpoint with lesser x will be potential connection point + while do or p != outerNode: + do = False + if hy <= p.y and hy >= p.next.y and p.next.y - p.y != 0: + x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y) + + if x <= hx and x > qx: + qx = x + + if (x == hx): + if hy == p.y: + return p + if hy == p.next.y: + return p.next + + m = p if p.x < p.next.x else p.next + + p = p.next + + if not m: + return None + + if hx == qx: + return m.prev # hole touches outer segment; pick lower endpoint + + # check points inside the triangle of hole point, segment intersection and endpoint + # if there are no points found, we have a valid connection + # otherwise choose the point of the minimum angle with the ray as connection point + + stop = m + mx = m.x + my = m.y + tanMin = float('inf') + tan = None + + p = m.next + + while p != stop: + hx_or_qx = hx if hy < my else qx + qx_or_hx = qx if hy < my else hx + + if hx >= p.x and p.x >= mx and \ + _point_in_triangle(hx_or_qx, hy, mx, my, qx_or_hx, hy, p.x, p.y): + try: + tan = abs(hy - p.y) / (hx - p.x) # tangential + except ZeroDivisionError: + break + + if (tan < tanMin or (tan == tanMin and p.x > m.x)) and \ + _locally_inside(p, hole): + m = p + tanMin = tan + + p = p.next + + return m + + +def _index_curve(start, minX, minY, size): + """Interlink polygon nodes in z-order.""" + do = True + p = start + + while do or p != start: + do = False + + if p.z is None: + p.z = _z_order(p.x, p.y, minX, minY, size) + + p.prevZ = p.prev + p.nextZ = p.next + p = p.next + + p.prevZ.nextZ = None + p.prevZ = None + + _sort_linked(p) + + +def _sort_linked(_list): + """Simon Tatham's linked list merge sort algorithm. + + More information available at https://www.chiark.greenend.org.uk/ + """ + do = True + i = None + p = None + q = None + e = None + tail = None + numMerges = None + pSize = None + qSize = None + inSize = 1 + + while do or numMerges > 1: + do = False + p = _list + _list = None + tail = None + numMerges = 0 + + while p: + numMerges += 1 + q = p + pSize = 0 + for i in range(inSize): + pSize += 1 + q = q.nextZ + if not q: + break + + qSize = inSize + + while pSize > 0 or (qSize > 0 and q): + + if pSize == 0: + e = q + q = q.nextZ + qSize -= 1 + + elif (qSize == 0 or not q): + e = p + p = p.nextZ + pSize -= 1 + + elif (p.z <= q.z): + e = p + p = p.nextZ + pSize -= 1 + + else: + e = q + q = q.nextZ + qSize -= 1 + + if tail: + tail.nextZ = e + + else: + _list = e + + e.prevZ = tail + tail = e + + p = q + + tail.nextZ = None + inSize *= 2 + + return _list + + +def _z_order(x, y, minX, minY, size): + """Z-order of a point given coords and size of the data bounding box.""" + # coords are transformed into non-negative 15-bit integer range + x = int(32767 * (x - minX) // size) + y = int(32767 * (y - minY) // size) + + x = (x | (x << 8)) & 0x00FF00FF + x = (x | (x << 4)) & 0x0F0F0F0F + x = (x | (x << 2)) & 0x33333333 + x = (x | (x << 1)) & 0x55555555 + + y = (y | (y << 8)) & 0x00FF00FF + y = (y | (y << 4)) & 0x0F0F0F0F + y = (y | (y << 2)) & 0x33333333 + y = (y | (y << 1)) & 0x55555555 + + return x | (y << 1) + + +def _get_leftmost(start): + """Find the leftmost node of a polygon ring.""" + do = True + p = start + leftmost = start + + while do or p != start: + do = False + if p.x < leftmost.x: + leftmost = p + p = p.next + + return leftmost + + +def _point_in_triangle(ax, ay, bx, by, cx, cy, px, py): + """Check if a point lies within a convex triangle.""" + return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 and \ + (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 and \ + (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0 + + +def _is_valid_diagonal(a, b): + """Check if a diagonal between two polygon nodes is valid. + + A valid diagonal is defined as one that lies in polygon interior. + """ + return a.next.i != b.i and a.prev.i != b.i and not _intersects_polygon(a, b) and \ + _locally_inside(a, b) and _locally_inside(b, a) and _middle_inside(a, b) + + +def _area(p, q, r): + """Get the signed area of a triangle.""" + return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y) + + +def _equals(p1, p2): + """Check if two points are equal.""" + return p1.x == p2.x and p1.y == p2.y + + +def _intersects(p1, q1, p2, q2): + """Check if two segments intersect.""" + if (_equals(p1, q1) and _equals(p2, q2)) or (_equals(p1, q2) and _equals(p2, q1)): + return True + + return _area(p1, q1, p2) > 0 != _area(p1, q1, q2) > 0 and \ + _area(p2, q2, p1) > 0 != _area(p2, q2, q1) > 0 + + +def _intersects_polygon(a, b): + """Check if a polygon diagonal intersects any polygon segments.""" + do = True + p = a + + while do or p != a: + do = False + init_int = p.i != a.i and p.next.i != a.i and p.i != b.i and p.next.i != b.i + if init_int and _intersects(p, p.next, a, b): + return True + + p = p.next + + return False + + +def _locally_inside(a, b): + """Check if a polygon diagonal is locally inside the polygon.""" + if _area(a.prev, a, a.next) < 0: + return _area(a, b, a.next) >= 0 and _area(a, a.prev, b) >= 0 + else: + return _area(a, b, a.prev) < 0 or _area(a, a.next, b) < 0 + + +def _middle_inside(a, b): + """Check if the middle point of a polygon diagonal is inside a polygon.""" + do = True + p = a + inside = False + px = (a.x + b.x) / 2 + py = (a.y + b.y) / 2 + + while do or p != a: + do = False + if ((p.y > py) != (p.next.y > py)) and \ + (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x): + inside = not inside + + p = p.next + + return inside + + +def _split_polygon(a, b): + """Link two polygon vertices with a bridge. + + If the vertices belong to the same ring, the polygon will be split into two. + If one belongs to the outer ring and another to a hole, the hole will be merged + into a single ring. + """ + a2 = _Node(a.i, a.x, a.y) + b2 = _Node(b.i, b.x, b.y) + an = a.next + bp = b.prev + + a.next = b + b.prev = a + + a2.next = an + an.prev = a2 + + b2.next = a2 + a2.prev = b2 + + bp.next = b2 + b2.prev = bp + + return b2 + + +def _insert_node(i, x, y, last): + """Create a node and optionally link it with previous one. + + Linking is done in a circular doubly linked list. + """ + p = _Node(i, x, y) + + if not last: + p.prev = p + p.next = p + + else: + p.next = last.next + p.prev = last + last.next.prev = p + last.next = p + + return p + + +def _remove_node(p): + """Remove a node from a list.""" + p.next.prev = p.prev + p.prev.next = p.next + + if p.prevZ: + p.prevZ.nextZ = p.nextZ + + if p.nextZ: + p.nextZ.prevZ = p.prevZ + + +class _Node(object): + """Node within a coordinate array.""" + + def __init__(self, i, x, y): + # vertex index in coordinates array + self.i = i + + # vertex coordinates + self.x = x + self.y = y + + # previous and next vertex nodes in a polygon ring + self.prev = None + self.next = None + + # z-order curve value + self.z = None + + # previous and next nodes in z-order + self.prevZ = None + self.nextZ = None + + # indicates whether this is a steiner point + self.steiner = False +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt new file mode 100644 index 00000000..92d8cc6b --- /dev/null +++ b/docs/_sources/index.rst.txt @@ -0,0 +1,27 @@ +Welcome to Ladybug Geometry's documentation! +============================================ + +.. image:: http://www.ladybug.tools/assets/img/ladybug.png + +Ladybug geometry is a Python library that houses geometry objects used throughout the +Ladybug Tools core libraries. + +Installation +============ + +``pip install -U ladybug-geometry`` + + +.. toctree:: + :maxdepth: 3 + :caption: Contents: + +.. include:: modules.rst + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/_sources/ladybug_geometry.boolean.rst.txt b/docs/_sources/ladybug_geometry.boolean.rst.txt new file mode 100644 index 00000000..a351694c --- /dev/null +++ b/docs/_sources/ladybug_geometry.boolean.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.boolean module +================================ + +.. automodule:: ladybug_geometry.boolean + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.bounding.rst.txt b/docs/_sources/ladybug_geometry.bounding.rst.txt new file mode 100644 index 00000000..e672f627 --- /dev/null +++ b/docs/_sources/ladybug_geometry.bounding.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.bounding module +================================= + +.. automodule:: ladybug_geometry.bounding + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.dictutil.rst.txt b/docs/_sources/ladybug_geometry.dictutil.rst.txt new file mode 100644 index 00000000..54ae664a --- /dev/null +++ b/docs/_sources/ladybug_geometry.dictutil.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.dictutil module +================================= + +.. automodule:: ladybug_geometry.dictutil + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry2d.arc.rst.txt b/docs/_sources/ladybug_geometry.geometry2d.arc.rst.txt new file mode 100644 index 00000000..1be20e47 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry2d.arc.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry2d.arc module +======================================= + +.. automodule:: ladybug_geometry.geometry2d.arc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry2d.line.rst.txt b/docs/_sources/ladybug_geometry.geometry2d.line.rst.txt new file mode 100644 index 00000000..9cbc64eb --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry2d.line.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry2d.line module +======================================== + +.. automodule:: ladybug_geometry.geometry2d.line + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry2d.mesh.rst.txt b/docs/_sources/ladybug_geometry.geometry2d.mesh.rst.txt new file mode 100644 index 00000000..744e00f5 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry2d.mesh.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry2d.mesh module +======================================== + +.. automodule:: ladybug_geometry.geometry2d.mesh + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry2d.pointvector.rst.txt b/docs/_sources/ladybug_geometry.geometry2d.pointvector.rst.txt new file mode 100644 index 00000000..fb39081a --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry2d.pointvector.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry2d.pointvector module +=============================================== + +.. automodule:: ladybug_geometry.geometry2d.pointvector + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry2d.polygon.rst.txt b/docs/_sources/ladybug_geometry.geometry2d.polygon.rst.txt new file mode 100644 index 00000000..8f0c1431 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry2d.polygon.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry2d.polygon module +=========================================== + +.. automodule:: ladybug_geometry.geometry2d.polygon + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry2d.polyline.rst.txt b/docs/_sources/ladybug_geometry.geometry2d.polyline.rst.txt new file mode 100644 index 00000000..1cdda199 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry2d.polyline.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry2d.polyline module +============================================ + +.. automodule:: ladybug_geometry.geometry2d.polyline + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry2d.ray.rst.txt b/docs/_sources/ladybug_geometry.geometry2d.ray.rst.txt new file mode 100644 index 00000000..1d2c3c01 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry2d.ray.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry2d.ray module +======================================= + +.. automodule:: ladybug_geometry.geometry2d.ray + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry2d.rst.txt b/docs/_sources/ladybug_geometry.geometry2d.rst.txt new file mode 100644 index 00000000..0819d347 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry2d.rst.txt @@ -0,0 +1,24 @@ +ladybug\_geometry.geometry2d package +==================================== + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + ladybug_geometry.geometry2d.arc + ladybug_geometry.geometry2d.line + ladybug_geometry.geometry2d.mesh + ladybug_geometry.geometry2d.pointvector + ladybug_geometry.geometry2d.polygon + ladybug_geometry.geometry2d.polyline + ladybug_geometry.geometry2d.ray + +Module contents +--------------- + +.. automodule:: ladybug_geometry.geometry2d + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.arc.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.arc.rst.txt new file mode 100644 index 00000000..1677c324 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.arc.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.arc module +======================================= + +.. automodule:: ladybug_geometry.geometry3d.arc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.cone.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.cone.rst.txt new file mode 100644 index 00000000..7b3160cc --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.cone.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.cone module +======================================== + +.. automodule:: ladybug_geometry.geometry3d.cone + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.cylinder.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.cylinder.rst.txt new file mode 100644 index 00000000..4cdc9129 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.cylinder.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.cylinder module +============================================ + +.. automodule:: ladybug_geometry.geometry3d.cylinder + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.face.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.face.rst.txt new file mode 100644 index 00000000..5c28d374 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.face.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.face module +======================================== + +.. automodule:: ladybug_geometry.geometry3d.face + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.line.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.line.rst.txt new file mode 100644 index 00000000..b03c533f --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.line.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.line module +======================================== + +.. automodule:: ladybug_geometry.geometry3d.line + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.mesh.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.mesh.rst.txt new file mode 100644 index 00000000..bbcdb3c6 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.mesh.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.mesh module +======================================== + +.. automodule:: ladybug_geometry.geometry3d.mesh + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.plane.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.plane.rst.txt new file mode 100644 index 00000000..40d28b44 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.plane.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.plane module +========================================= + +.. automodule:: ladybug_geometry.geometry3d.plane + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.pointvector.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.pointvector.rst.txt new file mode 100644 index 00000000..93eedff6 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.pointvector.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.pointvector module +=============================================== + +.. automodule:: ladybug_geometry.geometry3d.pointvector + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.polyface.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.polyface.rst.txt new file mode 100644 index 00000000..2390b569 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.polyface.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.polyface module +============================================ + +.. automodule:: ladybug_geometry.geometry3d.polyface + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.polyline.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.polyline.rst.txt new file mode 100644 index 00000000..40018e21 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.polyline.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.polyline module +============================================ + +.. automodule:: ladybug_geometry.geometry3d.polyline + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.ray.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.ray.rst.txt new file mode 100644 index 00000000..98508335 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.ray.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.ray module +======================================= + +.. automodule:: ladybug_geometry.geometry3d.ray + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.rst.txt new file mode 100644 index 00000000..f0092fdb --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.rst.txt @@ -0,0 +1,29 @@ +ladybug\_geometry.geometry3d package +==================================== + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + ladybug_geometry.geometry3d.arc + ladybug_geometry.geometry3d.cone + ladybug_geometry.geometry3d.cylinder + ladybug_geometry.geometry3d.face + ladybug_geometry.geometry3d.line + ladybug_geometry.geometry3d.mesh + ladybug_geometry.geometry3d.plane + ladybug_geometry.geometry3d.pointvector + ladybug_geometry.geometry3d.polyface + ladybug_geometry.geometry3d.polyline + ladybug_geometry.geometry3d.ray + ladybug_geometry.geometry3d.sphere + +Module contents +--------------- + +.. automodule:: ladybug_geometry.geometry3d + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.geometry3d.sphere.rst.txt b/docs/_sources/ladybug_geometry.geometry3d.sphere.rst.txt new file mode 100644 index 00000000..1315dfe2 --- /dev/null +++ b/docs/_sources/ladybug_geometry.geometry3d.sphere.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.geometry3d.sphere module +========================================== + +.. automodule:: ladybug_geometry.geometry3d.sphere + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.interop.obj.rst.txt b/docs/_sources/ladybug_geometry.interop.obj.rst.txt new file mode 100644 index 00000000..9ef56c87 --- /dev/null +++ b/docs/_sources/ladybug_geometry.interop.obj.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.interop.obj module +==================================== + +.. automodule:: ladybug_geometry.interop.obj + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.interop.rst.txt b/docs/_sources/ladybug_geometry.interop.rst.txt new file mode 100644 index 00000000..0f86219f --- /dev/null +++ b/docs/_sources/ladybug_geometry.interop.rst.txt @@ -0,0 +1,19 @@ +ladybug\_geometry.interop package +================================= + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + ladybug_geometry.interop.obj + ladybug_geometry.interop.stl + +Module contents +--------------- + +.. automodule:: ladybug_geometry.interop + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.interop.stl.rst.txt b/docs/_sources/ladybug_geometry.interop.stl.rst.txt new file mode 100644 index 00000000..21fd2b51 --- /dev/null +++ b/docs/_sources/ladybug_geometry.interop.stl.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.interop.stl module +==================================== + +.. automodule:: ladybug_geometry.interop.stl + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.intersection2d.rst.txt b/docs/_sources/ladybug_geometry.intersection2d.rst.txt new file mode 100644 index 00000000..baa85431 --- /dev/null +++ b/docs/_sources/ladybug_geometry.intersection2d.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.intersection2d module +======================================= + +.. automodule:: ladybug_geometry.intersection2d + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.intersection3d.rst.txt b/docs/_sources/ladybug_geometry.intersection3d.rst.txt new file mode 100644 index 00000000..96f7bcfd --- /dev/null +++ b/docs/_sources/ladybug_geometry.intersection3d.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.intersection3d module +======================================= + +.. automodule:: ladybug_geometry.intersection3d + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.network.rst.txt b/docs/_sources/ladybug_geometry.network.rst.txt new file mode 100644 index 00000000..68bcfa06 --- /dev/null +++ b/docs/_sources/ladybug_geometry.network.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.network module +================================ + +.. automodule:: ladybug_geometry.network + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.rst.txt b/docs/_sources/ladybug_geometry.rst.txt new file mode 100644 index 00000000..119f3ab8 --- /dev/null +++ b/docs/_sources/ladybug_geometry.rst.txt @@ -0,0 +1,34 @@ +ladybug\_geometry package +========================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + ladybug_geometry.geometry2d + ladybug_geometry.geometry3d + ladybug_geometry.interop + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + ladybug_geometry.boolean + ladybug_geometry.bounding + ladybug_geometry.dictutil + ladybug_geometry.intersection2d + ladybug_geometry.intersection3d + ladybug_geometry.network + ladybug_geometry.triangulation + +Module contents +--------------- + +.. automodule:: ladybug_geometry + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/ladybug_geometry.triangulation.rst.txt b/docs/_sources/ladybug_geometry.triangulation.rst.txt new file mode 100644 index 00000000..aeb86043 --- /dev/null +++ b/docs/_sources/ladybug_geometry.triangulation.rst.txt @@ -0,0 +1,7 @@ +ladybug\_geometry.triangulation module +====================================== + +.. automodule:: ladybug_geometry.triangulation + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/modules.rst.txt b/docs/_sources/modules.rst.txt new file mode 100644 index 00000000..886020fa --- /dev/null +++ b/docs/_sources/modules.rst.txt @@ -0,0 +1,7 @@ +ladybug_geometry +================ + +.. toctree:: + :maxdepth: 4 + + ladybug_geometry diff --git a/docs/_static/basic.css b/docs/_static/basic.css new file mode 100644 index 00000000..f316efcb --- /dev/null +++ b/docs/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.css b/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.css new file mode 100644 index 00000000..09e88ce3 --- /dev/null +++ b/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.css @@ -0,0 +1,1109 @@ +/*! + * Bootstrap Responsive v2.3.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +@-ms-viewport { + width: device-width; +} + +.hidden { + display: none; + visibility: hidden; +} + +.visible-phone { + display: none !important; +} + +.visible-tablet { + display: none !important; +} + +.hidden-desktop { + display: none !important; +} + +.visible-desktop { + display: inherit !important; +} + +@media (min-width: 768px) and (max-width: 979px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important ; + } + .visible-tablet { + display: inherit !important; + } + .hidden-tablet { + display: none !important; + } +} + +@media (max-width: 767px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important; + } + .visible-phone { + display: inherit !important; + } + .hidden-phone { + display: none !important; + } +} + +.visible-print { + display: none !important; +} + +@media print { + .visible-print { + display: inherit !important; + } + .hidden-print { + display: none !important; + } +} + +@media (min-width: 1200px) { + .row { + margin-left: -30px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 30px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 1170px; + } + .span12 { + width: 1170px; + } + .span11 { + width: 1070px; + } + .span10 { + width: 970px; + } + .span9 { + width: 870px; + } + .span8 { + width: 770px; + } + .span7 { + width: 670px; + } + .span6 { + width: 570px; + } + .span5 { + width: 470px; + } + .span4 { + width: 370px; + } + .span3 { + width: 270px; + } + .span2 { + width: 170px; + } + .span1 { + width: 70px; + } + .offset12 { + margin-left: 1230px; + } + .offset11 { + margin-left: 1130px; + } + .offset10 { + margin-left: 1030px; + } + .offset9 { + margin-left: 930px; + } + .offset8 { + margin-left: 830px; + } + .offset7 { + margin-left: 730px; + } + .offset6 { + margin-left: 630px; + } + .offset5 { + margin-left: 530px; + } + .offset4 { + margin-left: 430px; + } + .offset3 { + margin-left: 330px; + } + .offset2 { + margin-left: 230px; + } + .offset1 { + margin-left: 130px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.564102564102564%; + *margin-left: 2.5109110747408616%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.564102564102564%; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.45299145299145%; + *width: 91.39979996362975%; + } + .row-fluid .span10 { + width: 82.90598290598291%; + *width: 82.8527914166212%; + } + .row-fluid .span9 { + width: 74.35897435897436%; + *width: 74.30578286961266%; + } + .row-fluid .span8 { + width: 65.81196581196582%; + *width: 65.75877432260411%; + } + .row-fluid .span7 { + width: 57.26495726495726%; + *width: 57.21176577559556%; + } + .row-fluid .span6 { + width: 48.717948717948715%; + *width: 48.664757228587014%; + } + .row-fluid .span5 { + width: 40.17094017094017%; + *width: 40.11774868157847%; + } + .row-fluid .span4 { + width: 31.623931623931625%; + *width: 31.570740134569924%; + } + .row-fluid .span3 { + width: 23.076923076923077%; + *width: 23.023731587561375%; + } + .row-fluid .span2 { + width: 14.52991452991453%; + *width: 14.476723040552828%; + } + .row-fluid .span1 { + width: 5.982905982905983%; + *width: 5.929714493544281%; + } + .row-fluid .offset12 { + margin-left: 105.12820512820512%; + *margin-left: 105.02182214948171%; + } + .row-fluid .offset12:first-child { + margin-left: 102.56410256410257%; + *margin-left: 102.45771958537915%; + } + .row-fluid .offset11 { + margin-left: 96.58119658119658%; + *margin-left: 96.47481360247316%; + } + .row-fluid .offset11:first-child { + margin-left: 94.01709401709402%; + *margin-left: 93.91071103837061%; + } + .row-fluid .offset10 { + margin-left: 88.03418803418803%; + *margin-left: 87.92780505546462%; + } + .row-fluid .offset10:first-child { + margin-left: 85.47008547008548%; + *margin-left: 85.36370249136206%; + } + .row-fluid .offset9 { + margin-left: 79.48717948717949%; + *margin-left: 79.38079650845607%; + } + .row-fluid .offset9:first-child { + margin-left: 76.92307692307693%; + *margin-left: 76.81669394435352%; + } + .row-fluid .offset8 { + margin-left: 70.94017094017094%; + *margin-left: 70.83378796144753%; + } + .row-fluid .offset8:first-child { + margin-left: 68.37606837606839%; + *margin-left: 68.26968539734497%; + } + .row-fluid .offset7 { + margin-left: 62.393162393162385%; + *margin-left: 62.28677941443899%; + } + .row-fluid .offset7:first-child { + margin-left: 59.82905982905982%; + *margin-left: 59.72267685033642%; + } + .row-fluid .offset6 { + margin-left: 53.84615384615384%; + *margin-left: 53.739770867430444%; + } + .row-fluid .offset6:first-child { + margin-left: 51.28205128205128%; + *margin-left: 51.175668303327875%; + } + .row-fluid .offset5 { + margin-left: 45.299145299145295%; + *margin-left: 45.1927623204219%; + } + .row-fluid .offset5:first-child { + margin-left: 42.73504273504273%; + *margin-left: 42.62865975631933%; + } + .row-fluid .offset4 { + margin-left: 36.75213675213675%; + *margin-left: 36.645753773413354%; + } + .row-fluid .offset4:first-child { + margin-left: 34.18803418803419%; + *margin-left: 34.081651209310785%; + } + .row-fluid .offset3 { + margin-left: 28.205128205128204%; + *margin-left: 28.0987452264048%; + } + .row-fluid .offset3:first-child { + margin-left: 25.641025641025642%; + *margin-left: 25.53464266230224%; + } + .row-fluid .offset2 { + margin-left: 19.65811965811966%; + *margin-left: 19.551736679396257%; + } + .row-fluid .offset2:first-child { + margin-left: 17.094017094017094%; + *margin-left: 16.98763411529369%; + } + .row-fluid .offset1 { + margin-left: 11.11111111111111%; + *margin-left: 11.004728132387708%; + } + .row-fluid .offset1:first-child { + margin-left: 8.547008547008547%; + *margin-left: 8.440625568285142%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 30px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 1156px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 1056px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 956px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 856px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 756px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 656px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 556px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 456px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 356px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 256px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 156px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 56px; + } + .thumbnails { + margin-left: -30px; + } + .thumbnails > li { + margin-left: 30px; + } + .row-fluid .thumbnails { + margin-left: 0; + } +} + +@media (min-width: 768px) and (max-width: 979px) { + .row { + margin-left: -20px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 724px; + } + .span12 { + width: 724px; + } + .span11 { + width: 662px; + } + .span10 { + width: 600px; + } + .span9 { + width: 538px; + } + .span8 { + width: 476px; + } + .span7 { + width: 414px; + } + .span6 { + width: 352px; + } + .span5 { + width: 290px; + } + .span4 { + width: 228px; + } + .span3 { + width: 166px; + } + .span2 { + width: 104px; + } + .span1 { + width: 42px; + } + .offset12 { + margin-left: 764px; + } + .offset11 { + margin-left: 702px; + } + .offset10 { + margin-left: 640px; + } + .offset9 { + margin-left: 578px; + } + .offset8 { + margin-left: 516px; + } + .offset7 { + margin-left: 454px; + } + .offset6 { + margin-left: 392px; + } + .offset5 { + margin-left: 330px; + } + .offset4 { + margin-left: 268px; + } + .offset3 { + margin-left: 206px; + } + .offset2 { + margin-left: 144px; + } + .offset1 { + margin-left: 82px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.7624309392265194%; + *margin-left: 2.709239449864817%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.7624309392265194%; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.43646408839778%; + *width: 91.38327259903608%; + } + .row-fluid .span10 { + width: 82.87292817679558%; + *width: 82.81973668743387%; + } + .row-fluid .span9 { + width: 74.30939226519337%; + *width: 74.25620077583166%; + } + .row-fluid .span8 { + width: 65.74585635359117%; + *width: 65.69266486422946%; + } + .row-fluid .span7 { + width: 57.18232044198895%; + *width: 57.12912895262725%; + } + .row-fluid .span6 { + width: 48.61878453038674%; + *width: 48.56559304102504%; + } + .row-fluid .span5 { + width: 40.05524861878453%; + *width: 40.00205712942283%; + } + .row-fluid .span4 { + width: 31.491712707182323%; + *width: 31.43852121782062%; + } + .row-fluid .span3 { + width: 22.92817679558011%; + *width: 22.87498530621841%; + } + .row-fluid .span2 { + width: 14.3646408839779%; + *width: 14.311449394616199%; + } + .row-fluid .span1 { + width: 5.801104972375691%; + *width: 5.747913483013988%; + } + .row-fluid .offset12 { + margin-left: 105.52486187845304%; + *margin-left: 105.41847889972962%; + } + .row-fluid .offset12:first-child { + margin-left: 102.76243093922652%; + *margin-left: 102.6560479605031%; + } + .row-fluid .offset11 { + margin-left: 96.96132596685082%; + *margin-left: 96.8549429881274%; + } + .row-fluid .offset11:first-child { + margin-left: 94.1988950276243%; + *margin-left: 94.09251204890089%; + } + .row-fluid .offset10 { + margin-left: 88.39779005524862%; + *margin-left: 88.2914070765252%; + } + .row-fluid .offset10:first-child { + margin-left: 85.6353591160221%; + *margin-left: 85.52897613729868%; + } + .row-fluid .offset9 { + margin-left: 79.8342541436464%; + *margin-left: 79.72787116492299%; + } + .row-fluid .offset9:first-child { + margin-left: 77.07182320441989%; + *margin-left: 76.96544022569647%; + } + .row-fluid .offset8 { + margin-left: 71.2707182320442%; + *margin-left: 71.16433525332079%; + } + .row-fluid .offset8:first-child { + margin-left: 68.50828729281768%; + *margin-left: 68.40190431409427%; + } + .row-fluid .offset7 { + margin-left: 62.70718232044199%; + *margin-left: 62.600799341718584%; + } + .row-fluid .offset7:first-child { + margin-left: 59.94475138121547%; + *margin-left: 59.838368402492065%; + } + .row-fluid .offset6 { + margin-left: 54.14364640883978%; + *margin-left: 54.037263430116376%; + } + .row-fluid .offset6:first-child { + margin-left: 51.38121546961326%; + *margin-left: 51.27483249088986%; + } + .row-fluid .offset5 { + margin-left: 45.58011049723757%; + *margin-left: 45.47372751851417%; + } + .row-fluid .offset5:first-child { + margin-left: 42.81767955801105%; + *margin-left: 42.71129657928765%; + } + .row-fluid .offset4 { + margin-left: 37.01657458563536%; + *margin-left: 36.91019160691196%; + } + .row-fluid .offset4:first-child { + margin-left: 34.25414364640884%; + *margin-left: 34.14776066768544%; + } + .row-fluid .offset3 { + margin-left: 28.45303867403315%; + *margin-left: 28.346655695309746%; + } + .row-fluid .offset3:first-child { + margin-left: 25.69060773480663%; + *margin-left: 25.584224756083227%; + } + .row-fluid .offset2 { + margin-left: 19.88950276243094%; + *margin-left: 19.783119783707537%; + } + .row-fluid .offset2:first-child { + margin-left: 17.12707182320442%; + *margin-left: 17.02068884448102%; + } + .row-fluid .offset1 { + margin-left: 11.32596685082873%; + *margin-left: 11.219583872105325%; + } + .row-fluid .offset1:first-child { + margin-left: 8.56353591160221%; + *margin-left: 8.457152932878806%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 710px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 648px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 586px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 524px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 462px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 400px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 338px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 276px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 214px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 152px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 90px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 28px; + } +} + +@media (max-width: 767px) { + body { + padding-right: 20px; + padding-left: 20px; + } + .navbar-fixed-top, + .navbar-fixed-bottom, + .navbar-static-top { + margin-right: -20px; + margin-left: -20px; + } + .container-fluid { + padding: 0; + } + .dl-horizontal dt { + float: none; + width: auto; + clear: none; + text-align: left; + } + .dl-horizontal dd { + margin-left: 0; + } + .container { + width: auto; + } + .row-fluid { + width: 100%; + } + .row, + .thumbnails { + margin-left: 0; + } + .thumbnails > li { + float: none; + margin-left: 0; + } + [class*="span"], + .uneditable-input[class*="span"], + .row-fluid [class*="span"] { + display: block; + float: none; + width: 100%; + margin-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .span12, + .row-fluid .span12 { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="offset"]:first-child { + margin-left: 0; + } + .input-large, + .input-xlarge, + .input-xxlarge, + input[class*="span"], + select[class*="span"], + textarea[class*="span"], + .uneditable-input { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .input-prepend input, + .input-append input, + .input-prepend input[class*="span"], + .input-append input[class*="span"] { + display: inline-block; + width: auto; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 0; + } + .modal { + position: fixed; + top: 20px; + right: 20px; + left: 20px; + width: auto; + margin: 0; + } + .modal.fade { + top: -100px; + } + .modal.fade.in { + top: 20px; + } +} + +@media (max-width: 480px) { + .nav-collapse { + -webkit-transform: translate3d(0, 0, 0); + } + .page-header h1 small { + display: block; + line-height: 20px; + } + input[type="checkbox"], + input[type="radio"] { + border: 1px solid #ccc; + } + .form-horizontal .control-label { + float: none; + width: auto; + padding-top: 0; + text-align: left; + } + .form-horizontal .controls { + margin-left: 0; + } + .form-horizontal .control-list { + padding-top: 0; + } + .form-horizontal .form-actions { + padding-right: 10px; + padding-left: 10px; + } + .media .pull-left, + .media .pull-right { + display: block; + float: none; + margin-bottom: 10px; + } + .media-object { + margin-right: 0; + margin-left: 0; + } + .modal { + top: 10px; + right: 10px; + left: 10px; + } + .modal-header .close { + padding: 10px; + margin: -10px; + } + .carousel-caption { + position: static; + } +} + +@media (max-width: 979px) { + body { + padding-top: 0; + } + .navbar-fixed-top, + .navbar-fixed-bottom { + position: static; + } + .navbar-fixed-top { + margin-bottom: 20px; + } + .navbar-fixed-bottom { + margin-top: 20px; + } + .navbar-fixed-top .navbar-inner, + .navbar-fixed-bottom .navbar-inner { + padding: 5px; + } + .navbar .container { + width: auto; + padding: 0; + } + .navbar .brand { + padding-right: 10px; + padding-left: 10px; + margin: 0 0 0 -5px; + } + .nav-collapse { + clear: both; + } + .nav-collapse .nav { + float: none; + margin: 0 0 10px; + } + .nav-collapse .nav > li { + float: none; + } + .nav-collapse .nav > li > a { + margin-bottom: 2px; + } + .nav-collapse .nav > .divider-vertical { + display: none; + } + .nav-collapse .nav .nav-header { + color: #777777; + text-shadow: none; + } + .nav-collapse .nav > li > a, + .nav-collapse .dropdown-menu a { + padding: 9px 15px; + font-weight: bold; + color: #777777; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + .nav-collapse .btn { + padding: 4px 10px 4px; + font-weight: normal; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + } + .nav-collapse .dropdown-menu li + li a { + margin-bottom: 2px; + } + .nav-collapse .nav > li > a:hover, + .nav-collapse .nav > li > a:focus, + .nav-collapse .dropdown-menu a:hover, + .nav-collapse .dropdown-menu a:focus { + background-color: #f2f2f2; + } + .navbar-inverse .nav-collapse .nav > li > a, + .navbar-inverse .nav-collapse .dropdown-menu a { + color: #999999; + } + .navbar-inverse .nav-collapse .nav > li > a:hover, + .navbar-inverse .nav-collapse .nav > li > a:focus, + .navbar-inverse .nav-collapse .dropdown-menu a:hover, + .navbar-inverse .nav-collapse .dropdown-menu a:focus { + background-color: #111111; + } + .nav-collapse.in .btn-group { + padding: 0; + margin-top: 5px; + } + .nav-collapse .dropdown-menu { + position: static; + top: auto; + left: auto; + display: none; + float: none; + max-width: none; + padding: 0; + margin: 0 15px; + background-color: transparent; + border: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } + .nav-collapse .open > .dropdown-menu { + display: block; + } + .nav-collapse .dropdown-menu:before, + .nav-collapse .dropdown-menu:after { + display: none; + } + .nav-collapse .dropdown-menu .divider { + display: none; + } + .nav-collapse .nav > li > .dropdown-menu:before, + .nav-collapse .nav > li > .dropdown-menu:after { + display: none; + } + .nav-collapse .navbar-form, + .nav-collapse .navbar-search { + float: none; + padding: 10px 15px; + margin: 10px 0; + border-top: 1px solid #f2f2f2; + border-bottom: 1px solid #f2f2f2; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar-inverse .nav-collapse .navbar-form, + .navbar-inverse .nav-collapse .navbar-search { + border-top-color: #111111; + border-bottom-color: #111111; + } + .navbar .nav-collapse .nav.pull-right { + float: none; + margin-left: 0; + } + .nav-collapse, + .nav-collapse.collapse { + height: 0; + overflow: hidden; + } + .navbar .btn-navbar { + display: block; + } + .navbar-static .navbar-inner { + padding-right: 10px; + padding-left: 10px; + } +} + +@media (min-width: 980px) { + .nav-collapse.collapse { + height: auto !important; + overflow: visible !important; + } +} diff --git a/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.min.css b/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.min.css new file mode 100644 index 00000000..f4ede63f --- /dev/null +++ b/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap Responsive v2.3.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} diff --git a/docs/_static/bootstrap-2.3.2/css/bootstrap.css b/docs/_static/bootstrap-2.3.2/css/bootstrap.css new file mode 100644 index 00000000..b725064a --- /dev/null +++ b/docs/_static/bootstrap-2.3.2/css/bootstrap.css @@ -0,0 +1,6167 @@ +/*! + * Bootstrap v2.3.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +audio:not([controls]) { + display: none; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:hover, +a:active { + outline: 0; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + width: auto\9; + height: auto; + max-width: 100%; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} + +#map_canvas img, +.google-maps img { + max-width: none; +} + +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} + +button, +input { + *overflow: visible; + line-height: normal; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +label, +select, +button, +input[type="button"], +input[type="reset"], +input[type="submit"], +input[type="radio"], +input[type="checkbox"] { + cursor: pointer; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +@media print { + * { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + @page { + margin: 0.5cm; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } +} + +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 20px; + color: #333333; + background-color: #ffffff; +} + +a { + color: #0088cc; + text-decoration: none; +} + +a:hover, +a:focus { + color: #005580; + text-decoration: underline; +} + +.img-rounded { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.img-polaroid { + padding: 4px; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.img-circle { + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + border-radius: 500px; +} + +.row { + margin-left: -20px; + *zoom: 1; +} + +.row:before, +.row:after { + display: table; + line-height: 0; + content: ""; +} + +.row:after { + clear: both; +} + +[class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; +} + +.container, +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.span12 { + width: 940px; +} + +.span11 { + width: 860px; +} + +.span10 { + width: 780px; +} + +.span9 { + width: 700px; +} + +.span8 { + width: 620px; +} + +.span7 { + width: 540px; +} + +.span6 { + width: 460px; +} + +.span5 { + width: 380px; +} + +.span4 { + width: 300px; +} + +.span3 { + width: 220px; +} + +.span2 { + width: 140px; +} + +.span1 { + width: 60px; +} + +.offset12 { + margin-left: 980px; +} + +.offset11 { + margin-left: 900px; +} + +.offset10 { + margin-left: 820px; +} + +.offset9 { + margin-left: 740px; +} + +.offset8 { + margin-left: 660px; +} + +.offset7 { + margin-left: 580px; +} + +.offset6 { + margin-left: 500px; +} + +.offset5 { + margin-left: 420px; +} + +.offset4 { + margin-left: 340px; +} + +.offset3 { + margin-left: 260px; +} + +.offset2 { + margin-left: 180px; +} + +.offset1 { + margin-left: 100px; +} + +.row-fluid { + width: 100%; + *zoom: 1; +} + +.row-fluid:before, +.row-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.row-fluid:after { + clear: both; +} + +.row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.127659574468085%; + *margin-left: 2.074468085106383%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.row-fluid [class*="span"]:first-child { + margin-left: 0; +} + +.row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.127659574468085%; +} + +.row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; +} + +.row-fluid .span11 { + width: 91.48936170212765%; + *width: 91.43617021276594%; +} + +.row-fluid .span10 { + width: 82.97872340425532%; + *width: 82.92553191489361%; +} + +.row-fluid .span9 { + width: 74.46808510638297%; + *width: 74.41489361702126%; +} + +.row-fluid .span8 { + width: 65.95744680851064%; + *width: 65.90425531914893%; +} + +.row-fluid .span7 { + width: 57.44680851063829%; + *width: 57.39361702127659%; +} + +.row-fluid .span6 { + width: 48.93617021276595%; + *width: 48.88297872340425%; +} + +.row-fluid .span5 { + width: 40.42553191489362%; + *width: 40.37234042553192%; +} + +.row-fluid .span4 { + width: 31.914893617021278%; + *width: 31.861702127659576%; +} + +.row-fluid .span3 { + width: 23.404255319148934%; + *width: 23.351063829787233%; +} + +.row-fluid .span2 { + width: 14.893617021276595%; + *width: 14.840425531914894%; +} + +.row-fluid .span1 { + width: 6.382978723404255%; + *width: 6.329787234042553%; +} + +.row-fluid .offset12 { + margin-left: 104.25531914893617%; + *margin-left: 104.14893617021275%; +} + +.row-fluid .offset12:first-child { + margin-left: 102.12765957446808%; + *margin-left: 102.02127659574467%; +} + +.row-fluid .offset11 { + margin-left: 95.74468085106382%; + *margin-left: 95.6382978723404%; +} + +.row-fluid .offset11:first-child { + margin-left: 93.61702127659574%; + *margin-left: 93.51063829787232%; +} + +.row-fluid .offset10 { + margin-left: 87.23404255319149%; + *margin-left: 87.12765957446807%; +} + +.row-fluid .offset10:first-child { + margin-left: 85.1063829787234%; + *margin-left: 84.99999999999999%; +} + +.row-fluid .offset9 { + margin-left: 78.72340425531914%; + *margin-left: 78.61702127659572%; +} + +.row-fluid .offset9:first-child { + margin-left: 76.59574468085106%; + *margin-left: 76.48936170212764%; +} + +.row-fluid .offset8 { + margin-left: 70.2127659574468%; + *margin-left: 70.10638297872339%; +} + +.row-fluid .offset8:first-child { + margin-left: 68.08510638297872%; + *margin-left: 67.9787234042553%; +} + +.row-fluid .offset7 { + margin-left: 61.70212765957446%; + *margin-left: 61.59574468085106%; +} + +.row-fluid .offset7:first-child { + margin-left: 59.574468085106375%; + *margin-left: 59.46808510638297%; +} + +.row-fluid .offset6 { + margin-left: 53.191489361702125%; + *margin-left: 53.085106382978715%; +} + +.row-fluid .offset6:first-child { + margin-left: 51.063829787234035%; + *margin-left: 50.95744680851063%; +} + +.row-fluid .offset5 { + margin-left: 44.68085106382979%; + *margin-left: 44.57446808510638%; +} + +.row-fluid .offset5:first-child { + margin-left: 42.5531914893617%; + *margin-left: 42.4468085106383%; +} + +.row-fluid .offset4 { + margin-left: 36.170212765957444%; + *margin-left: 36.06382978723405%; +} + +.row-fluid .offset4:first-child { + margin-left: 34.04255319148936%; + *margin-left: 33.93617021276596%; +} + +.row-fluid .offset3 { + margin-left: 27.659574468085104%; + *margin-left: 27.5531914893617%; +} + +.row-fluid .offset3:first-child { + margin-left: 25.53191489361702%; + *margin-left: 25.425531914893618%; +} + +.row-fluid .offset2 { + margin-left: 19.148936170212764%; + *margin-left: 19.04255319148936%; +} + +.row-fluid .offset2:first-child { + margin-left: 17.02127659574468%; + *margin-left: 16.914893617021278%; +} + +.row-fluid .offset1 { + margin-left: 10.638297872340425%; + *margin-left: 10.53191489361702%; +} + +.row-fluid .offset1:first-child { + margin-left: 8.51063829787234%; + *margin-left: 8.404255319148938%; +} + +[class*="span"].hide, +.row-fluid [class*="span"].hide { + display: none; +} + +[class*="span"].pull-right, +.row-fluid [class*="span"].pull-right { + float: right; +} + +.container { + margin-right: auto; + margin-left: auto; + *zoom: 1; +} + +.container:before, +.container:after { + display: table; + line-height: 0; + content: ""; +} + +.container:after { + clear: both; +} + +.container-fluid { + padding-right: 20px; + padding-left: 20px; + *zoom: 1; +} + +.container-fluid:before, +.container-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.container-fluid:after { + clear: both; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 21px; + font-weight: 200; + line-height: 30px; +} + +small { + font-size: 85%; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +cite { + font-style: normal; +} + +.muted { + color: #999999; +} + +a.muted:hover, +a.muted:focus { + color: #808080; +} + +.text-warning { + color: #c09853; +} + +a.text-warning:hover, +a.text-warning:focus { + color: #a47e3c; +} + +.text-error { + color: #b94a48; +} + +a.text-error:hover, +a.text-error:focus { + color: #953b39; +} + +.text-info { + color: #3a87ad; +} + +a.text-info:hover, +a.text-info:focus { + color: #2d6987; +} + +.text-success { + color: #468847; +} + +a.text-success:hover, +a.text-success:focus { + color: #356635; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 10px 0; + font-family: inherit; + font-weight: bold; + line-height: 20px; + color: inherit; + text-rendering: optimizelegibility; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1, +h2, +h3 { + line-height: 40px; +} + +h1 { + font-size: 38.5px; +} + +h2 { + font-size: 31.5px; +} + +h3 { + font-size: 24.5px; +} + +h4 { + font-size: 17.5px; +} + +h5 { + font-size: 14px; +} + +h6 { + font-size: 11.9px; +} + +h1 small { + font-size: 24.5px; +} + +h2 small { + font-size: 17.5px; +} + +h3 small { + font-size: 14px; +} + +h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 20px 0 30px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + padding: 0; + margin: 0 0 10px 25px; +} + +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} + +li { + line-height: 20px; +} + +ul.unstyled, +ol.unstyled { + margin-left: 0; + list-style: none; +} + +ul.inline, +ol.inline { + margin-left: 0; + list-style: none; +} + +ul.inline > li, +ol.inline > li { + display: inline-block; + *display: inline; + padding-right: 5px; + padding-left: 5px; + *zoom: 1; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 20px; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 10px; +} + +.dl-horizontal { + *zoom: 1; +} + +.dl-horizontal:before, +.dl-horizontal:after { + display: table; + line-height: 0; + content: ""; +} + +.dl-horizontal:after { + clear: both; +} + +.dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dl-horizontal dd { + margin-left: 180px; +} + +hr { + margin: 20px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + margin-bottom: 0; + font-size: 17.5px; + font-weight: 300; + line-height: 1.25; +} + +blockquote small { + display: block; + line-height: 20px; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + float: right; + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 20px; +} + +code, +pre { + padding: 0 3px 2px; + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +code { + padding: 2px 4px; + color: #d14; + white-space: nowrap; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 20px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + color: inherit; + white-space: pre; + white-space: pre-wrap; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +form { + margin: 0 0 20px; +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: 40px; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +legend small { + font-size: 15px; + color: #999999; +} + +label, +input, +button, +select, +textarea { + font-size: 14px; + font-weight: normal; + line-height: 20px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +label { + display: block; + margin-bottom: 5px; +} + +select, +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + display: inline-block; + height: 20px; + padding: 4px 6px; + margin-bottom: 10px; + font-size: 14px; + line-height: 20px; + color: #555555; + vertical-align: middle; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +input, +textarea, +.uneditable-input { + width: 206px; +} + +textarea { + height: auto; +} + +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + background-color: #ffffff; + border: 1px solid #cccccc; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus, +.uneditable-input:focus { + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + *margin-top: 0; + line-height: normal; +} + +input[type="file"], +input[type="image"], +input[type="submit"], +input[type="reset"], +input[type="button"], +input[type="radio"], +input[type="checkbox"] { + width: auto; +} + +select, +input[type="file"] { + height: 30px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 30px; +} + +select { + width: 220px; + background-color: #ffffff; + border: 1px solid #cccccc; +} + +select[multiple], +select[size] { + height: auto; +} + +select:focus, +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.uneditable-input, +.uneditable-textarea { + color: #999999; + cursor: not-allowed; + background-color: #fcfcfc; + border-color: #cccccc; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); +} + +.uneditable-input { + overflow: hidden; + white-space: nowrap; +} + +.uneditable-textarea { + width: auto; + height: auto; +} + +input:-moz-placeholder, +textarea:-moz-placeholder { + color: #999999; +} + +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #999999; +} + +input::-webkit-input-placeholder, +textarea::-webkit-input-placeholder { + color: #999999; +} + +.radio, +.checkbox { + min-height: 20px; + padding-left: 20px; +} + +.radio input[type="radio"], +.checkbox input[type="checkbox"] { + float: left; + margin-left: -20px; +} + +.controls > .radio:first-child, +.controls > .checkbox:first-child { + padding-top: 5px; +} + +.radio.inline, +.checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} + +.radio.inline + .radio.inline, +.checkbox.inline + .checkbox.inline { + margin-left: 10px; +} + +.input-mini { + width: 60px; +} + +.input-small { + width: 90px; +} + +.input-medium { + width: 150px; +} + +.input-large { + width: 210px; +} + +.input-xlarge { + width: 270px; +} + +.input-xxlarge { + width: 530px; +} + +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"] { + float: none; + margin-left: 0; +} + +.input-append input[class*="span"], +.input-append .uneditable-input[class*="span"], +.input-prepend input[class*="span"], +.input-prepend .uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"], +.row-fluid .input-prepend [class*="span"], +.row-fluid .input-append [class*="span"] { + display: inline-block; +} + +input, +textarea, +.uneditable-input { + margin-left: 0; +} + +.controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; +} + +input.span12, +textarea.span12, +.uneditable-input.span12 { + width: 926px; +} + +input.span11, +textarea.span11, +.uneditable-input.span11 { + width: 846px; +} + +input.span10, +textarea.span10, +.uneditable-input.span10 { + width: 766px; +} + +input.span9, +textarea.span9, +.uneditable-input.span9 { + width: 686px; +} + +input.span8, +textarea.span8, +.uneditable-input.span8 { + width: 606px; +} + +input.span7, +textarea.span7, +.uneditable-input.span7 { + width: 526px; +} + +input.span6, +textarea.span6, +.uneditable-input.span6 { + width: 446px; +} + +input.span5, +textarea.span5, +.uneditable-input.span5 { + width: 366px; +} + +input.span4, +textarea.span4, +.uneditable-input.span4 { + width: 286px; +} + +input.span3, +textarea.span3, +.uneditable-input.span3 { + width: 206px; +} + +input.span2, +textarea.span2, +.uneditable-input.span2 { + width: 126px; +} + +input.span1, +textarea.span1, +.uneditable-input.span1 { + width: 46px; +} + +.controls-row { + *zoom: 1; +} + +.controls-row:before, +.controls-row:after { + display: table; + line-height: 0; + content: ""; +} + +.controls-row:after { + clear: both; +} + +.controls-row [class*="span"], +.row-fluid .controls-row [class*="span"] { + float: left; +} + +.controls-row .checkbox[class*="span"], +.controls-row .radio[class*="span"] { + padding-top: 5px; +} + +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #eeeeee; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { + background-color: transparent; +} + +.control-group.warning .control-label, +.control-group.warning .help-block, +.control-group.warning .help-inline { + color: #c09853; +} + +.control-group.warning .checkbox, +.control-group.warning .radio, +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + color: #c09853; +} + +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.warning input:focus, +.control-group.warning select:focus, +.control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.control-group.warning .input-prepend .add-on, +.control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.control-group.error .control-label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: #b94a48; +} + +.control-group.error .checkbox, +.control-group.error .radio, +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + color: #b94a48; +} + +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.error input:focus, +.control-group.error select:focus, +.control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.control-group.error .input-prepend .add-on, +.control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.control-group.success .control-label, +.control-group.success .help-block, +.control-group.success .help-inline { + color: #468847; +} + +.control-group.success .checkbox, +.control-group.success .radio, +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + color: #468847; +} + +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.success input:focus, +.control-group.success select:focus, +.control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.control-group.success .input-prepend .add-on, +.control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +.control-group.info .control-label, +.control-group.info .help-block, +.control-group.info .help-inline { + color: #3a87ad; +} + +.control-group.info .checkbox, +.control-group.info .radio, +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + color: #3a87ad; +} + +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + border-color: #3a87ad; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.info input:focus, +.control-group.info select:focus, +.control-group.info textarea:focus { + border-color: #2d6987; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; +} + +.control-group.info .input-prepend .add-on, +.control-group.info .input-append .add-on { + color: #3a87ad; + background-color: #d9edf7; + border-color: #3a87ad; +} + +input:focus:invalid, +textarea:focus:invalid, +select:focus:invalid { + color: #b94a48; + border-color: #ee5f5b; +} + +input:focus:invalid:focus, +textarea:focus:invalid:focus, +select:focus:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} + +.form-actions { + padding: 19px 20px 20px; + margin-top: 20px; + margin-bottom: 20px; + background-color: #f5f5f5; + border-top: 1px solid #e5e5e5; + *zoom: 1; +} + +.form-actions:before, +.form-actions:after { + display: table; + line-height: 0; + content: ""; +} + +.form-actions:after { + clear: both; +} + +.help-block, +.help-inline { + color: #595959; +} + +.help-block { + display: block; + margin-bottom: 10px; +} + +.help-inline { + display: inline-block; + *display: inline; + padding-left: 5px; + vertical-align: middle; + *zoom: 1; +} + +.input-append, +.input-prepend { + display: inline-block; + margin-bottom: 10px; + font-size: 0; + white-space: nowrap; + vertical-align: middle; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input, +.input-append .dropdown-menu, +.input-prepend .dropdown-menu, +.input-append .popover, +.input-prepend .popover { + font-size: 14px; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input { + position: relative; + margin-bottom: 0; + *margin-left: 0; + vertical-align: top; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append input:focus, +.input-prepend input:focus, +.input-append select:focus, +.input-prepend select:focus, +.input-append .uneditable-input:focus, +.input-prepend .uneditable-input:focus { + z-index: 2; +} + +.input-append .add-on, +.input-prepend .add-on { + display: inline-block; + width: auto; + height: 20px; + min-width: 16px; + padding: 4px 5px; + font-size: 14px; + font-weight: normal; + line-height: 20px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #eeeeee; + border: 1px solid #ccc; +} + +.input-append .add-on, +.input-prepend .add-on, +.input-append .btn, +.input-prepend .btn, +.input-append .btn-group > .dropdown-toggle, +.input-prepend .btn-group > .dropdown-toggle { + vertical-align: top; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-append .active, +.input-prepend .active { + background-color: #a9dba9; + border-color: #46a546; +} + +.input-prepend .add-on, +.input-prepend .btn { + margin-right: -1px; +} + +.input-prepend .add-on:first-child, +.input-prepend .btn:first-child { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input, +.input-append select, +.input-append .uneditable-input { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input + .btn-group .btn:last-child, +.input-append select + .btn-group .btn:last-child, +.input-append .uneditable-input + .btn-group .btn:last-child { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append .add-on, +.input-append .btn, +.input-append .btn-group { + margin-left: -1px; +} + +.input-append .add-on:last-child, +.input-append .btn:last-child, +.input-append .btn-group:last-child > .dropdown-toggle { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append input, +.input-prepend.input-append select, +.input-prepend.input-append .uneditable-input { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-prepend.input-append input + .btn-group .btn, +.input-prepend.input-append select + .btn-group .btn, +.input-prepend.input-append .uneditable-input + .btn-group .btn { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .add-on:first-child, +.input-prepend.input-append .btn:first-child { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-prepend.input-append .add-on:last-child, +.input-prepend.input-append .btn:last-child { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .btn-group:first-child { + margin-left: 0; +} + +input.search-query { + padding-right: 14px; + padding-right: 4px \9; + padding-left: 14px; + padding-left: 4px \9; + /* IE7-8 doesn't have border-radius, so don't indent the padding */ + + margin-bottom: 0; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +/* Allow for input prepend/append in search forms */ + +.form-search .input-append .search-query, +.form-search .input-prepend .search-query { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.form-search .input-append .search-query { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search .input-append .btn { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .search-query { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .btn { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input, +.form-search .input-prepend, +.form-inline .input-prepend, +.form-horizontal .input-prepend, +.form-search .input-append, +.form-inline .input-append, +.form-horizontal .input-append { + display: inline-block; + *display: inline; + margin-bottom: 0; + vertical-align: middle; + *zoom: 1; +} + +.form-search .hide, +.form-inline .hide, +.form-horizontal .hide { + display: none; +} + +.form-search label, +.form-inline label, +.form-search .btn-group, +.form-inline .btn-group { + display: inline-block; +} + +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + margin-bottom: 0; +} + +.form-search .radio, +.form-search .checkbox, +.form-inline .radio, +.form-inline .checkbox { + padding-left: 0; + margin-bottom: 0; + vertical-align: middle; +} + +.form-search .radio input[type="radio"], +.form-search .checkbox input[type="checkbox"], +.form-inline .radio input[type="radio"], +.form-inline .checkbox input[type="checkbox"] { + float: left; + margin-right: 3px; + margin-left: 0; +} + +.control-group { + margin-bottom: 10px; +} + +legend + .control-group { + margin-top: 20px; + -webkit-margin-top-collapse: separate; +} + +.form-horizontal .control-group { + margin-bottom: 20px; + *zoom: 1; +} + +.form-horizontal .control-group:before, +.form-horizontal .control-group:after { + display: table; + line-height: 0; + content: ""; +} + +.form-horizontal .control-group:after { + clear: both; +} + +.form-horizontal .control-label { + float: left; + width: 160px; + padding-top: 5px; + text-align: right; +} + +.form-horizontal .controls { + *display: inline-block; + *padding-left: 20px; + margin-left: 180px; + *margin-left: 0; +} + +.form-horizontal .controls:first-child { + *padding-left: 180px; +} + +.form-horizontal .help-block { + margin-bottom: 0; +} + +.form-horizontal input + .help-block, +.form-horizontal select + .help-block, +.form-horizontal textarea + .help-block, +.form-horizontal .uneditable-input + .help-block, +.form-horizontal .input-prepend + .help-block, +.form-horizontal .input-append + .help-block { + margin-top: 10px; +} + +.form-horizontal .form-actions { + padding-left: 180px; +} + +table { + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table th, +.table td { + padding: 8px; + line-height: 20px; + text-align: left; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table th { + font-weight: bold; +} + +.table thead th { + vertical-align: bottom; +} + +.table caption + thead tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child th, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child th, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table .table { + background-color: #ffffff; +} + +.table-condensed th, +.table-condensed td { + padding: 4px 5px; +} + +.table-bordered { + border: 1px solid #dddddd; + border-collapse: separate; + *border-collapse: collapse; + border-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.table-bordered th, +.table-bordered td { + border-left: 1px solid #dddddd; +} + +.table-bordered caption + thead tr:first-child th, +.table-bordered caption + tbody tr:first-child th, +.table-bordered caption + tbody tr:first-child td, +.table-bordered colgroup + thead tr:first-child th, +.table-bordered colgroup + tbody tr:first-child th, +.table-bordered colgroup + tbody tr:first-child td, +.table-bordered thead:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} + +.table-bordered thead:first-child tr:first-child > th:first-child, +.table-bordered tbody:first-child tr:first-child > td:first-child, +.table-bordered tbody:first-child tr:first-child > th:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered thead:first-child tr:first-child > th:last-child, +.table-bordered tbody:first-child tr:first-child > td:last-child, +.table-bordered tbody:first-child tr:first-child > th:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:first-child, +.table-bordered tbody:last-child tr:last-child > td:first-child, +.table-bordered tbody:last-child tr:last-child > th:first-child, +.table-bordered tfoot:last-child tr:last-child > td:first-child, +.table-bordered tfoot:last-child tr:last-child > th:first-child { + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:last-child, +.table-bordered tbody:last-child tr:last-child > td:last-child, +.table-bordered tbody:last-child tr:last-child > th:last-child, +.table-bordered tfoot:last-child tr:last-child > td:last-child, +.table-bordered tfoot:last-child tr:last-child > th:last-child { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:first-child { + -webkit-border-bottom-left-radius: 0; + border-bottom-left-radius: 0; + -moz-border-radius-bottomleft: 0; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:last-child { + -webkit-border-bottom-right-radius: 0; + border-bottom-right-radius: 0; + -moz-border-radius-bottomright: 0; +} + +.table-bordered caption + thead tr:first-child th:first-child, +.table-bordered caption + tbody tr:first-child td:first-child, +.table-bordered colgroup + thead tr:first-child th:first-child, +.table-bordered colgroup + tbody tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered caption + thead tr:first-child th:last-child, +.table-bordered caption + tbody tr:first-child td:last-child, +.table-bordered colgroup + thead tr:first-child th:last-child, +.table-bordered colgroup + tbody tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-striped tbody > tr:nth-child(odd) > td, +.table-striped tbody > tr:nth-child(odd) > th { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover > td, +.table-hover tbody tr:hover > th { + background-color: #f5f5f5; +} + +table td[class*="span"], +table th[class*="span"], +.row-fluid table td[class*="span"], +.row-fluid table th[class*="span"] { + display: table-cell; + float: none; + margin-left: 0; +} + +.table td.span1, +.table th.span1 { + float: none; + width: 44px; + margin-left: 0; +} + +.table td.span2, +.table th.span2 { + float: none; + width: 124px; + margin-left: 0; +} + +.table td.span3, +.table th.span3 { + float: none; + width: 204px; + margin-left: 0; +} + +.table td.span4, +.table th.span4 { + float: none; + width: 284px; + margin-left: 0; +} + +.table td.span5, +.table th.span5 { + float: none; + width: 364px; + margin-left: 0; +} + +.table td.span6, +.table th.span6 { + float: none; + width: 444px; + margin-left: 0; +} + +.table td.span7, +.table th.span7 { + float: none; + width: 524px; + margin-left: 0; +} + +.table td.span8, +.table th.span8 { + float: none; + width: 604px; + margin-left: 0; +} + +.table td.span9, +.table th.span9 { + float: none; + width: 684px; + margin-left: 0; +} + +.table td.span10, +.table th.span10 { + float: none; + width: 764px; + margin-left: 0; +} + +.table td.span11, +.table th.span11 { + float: none; + width: 844px; + margin-left: 0; +} + +.table td.span12, +.table th.span12 { + float: none; + width: 924px; + margin-left: 0; +} + +.table tbody tr.success > td { + background-color: #dff0d8; +} + +.table tbody tr.error > td { + background-color: #f2dede; +} + +.table tbody tr.warning > td { + background-color: #fcf8e3; +} + +.table tbody tr.info > td { + background-color: #d9edf7; +} + +.table-hover tbody tr.success:hover > td { + background-color: #d0e9c6; +} + +.table-hover tbody tr.error:hover > td { + background-color: #ebcccc; +} + +.table-hover tbody tr.warning:hover > td { + background-color: #faf2cc; +} + +.table-hover tbody tr.info:hover > td { + background-color: #c4e3f3; +} + +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + margin-top: 1px; + *margin-right: .3em; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; +} + +/* White icons with optional class, or on hover/focus/active states of certain elements */ + +.icon-white, +.nav-pills > .active > a > [class^="icon-"], +.nav-pills > .active > a > [class*=" icon-"], +.nav-list > .active > a > [class^="icon-"], +.nav-list > .active > a > [class*=" icon-"], +.navbar-inverse .nav > .active > a > [class^="icon-"], +.navbar-inverse .nav > .active > a > [class*=" icon-"], +.dropdown-menu > li > a:hover > [class^="icon-"], +.dropdown-menu > li > a:focus > [class^="icon-"], +.dropdown-menu > li > a:hover > [class*=" icon-"], +.dropdown-menu > li > a:focus > [class*=" icon-"], +.dropdown-menu > .active > a > [class^="icon-"], +.dropdown-menu > .active > a > [class*=" icon-"], +.dropdown-submenu:hover > a > [class^="icon-"], +.dropdown-submenu:focus > a > [class^="icon-"], +.dropdown-submenu:hover > a > [class*=" icon-"], +.dropdown-submenu:focus > a > [class*=" icon-"] { + background-image: url("../img/glyphicons-halflings-white.png"); +} + +.icon-glass { + background-position: 0 0; +} + +.icon-music { + background-position: -24px 0; +} + +.icon-search { + background-position: -48px 0; +} + +.icon-envelope { + background-position: -72px 0; +} + +.icon-heart { + background-position: -96px 0; +} + +.icon-star { + background-position: -120px 0; +} + +.icon-star-empty { + background-position: -144px 0; +} + +.icon-user { + background-position: -168px 0; +} + +.icon-film { + background-position: -192px 0; +} + +.icon-th-large { + background-position: -216px 0; +} + +.icon-th { + background-position: -240px 0; +} + +.icon-th-list { + background-position: -264px 0; +} + +.icon-ok { + background-position: -288px 0; +} + +.icon-remove { + background-position: -312px 0; +} + +.icon-zoom-in { + background-position: -336px 0; +} + +.icon-zoom-out { + background-position: -360px 0; +} + +.icon-off { + background-position: -384px 0; +} + +.icon-signal { + background-position: -408px 0; +} + +.icon-cog { + background-position: -432px 0; +} + +.icon-trash { + background-position: -456px 0; +} + +.icon-home { + background-position: 0 -24px; +} + +.icon-file { + background-position: -24px -24px; +} + +.icon-time { + background-position: -48px -24px; +} + +.icon-road { + background-position: -72px -24px; +} + +.icon-download-alt { + background-position: -96px -24px; +} + +.icon-download { + background-position: -120px -24px; +} + +.icon-upload { + background-position: -144px -24px; +} + +.icon-inbox { + background-position: -168px -24px; +} + +.icon-play-circle { + background-position: -192px -24px; +} + +.icon-repeat { + background-position: -216px -24px; +} + +.icon-refresh { + background-position: -240px -24px; +} + +.icon-list-alt { + background-position: -264px -24px; +} + +.icon-lock { + background-position: -287px -24px; +} + +.icon-flag { + background-position: -312px -24px; +} + +.icon-headphones { + background-position: -336px -24px; +} + +.icon-volume-off { + background-position: -360px -24px; +} + +.icon-volume-down { + background-position: -384px -24px; +} + +.icon-volume-up { + background-position: -408px -24px; +} + +.icon-qrcode { + background-position: -432px -24px; +} + +.icon-barcode { + background-position: -456px -24px; +} + +.icon-tag { + background-position: 0 -48px; +} + +.icon-tags { + background-position: -25px -48px; +} + +.icon-book { + background-position: -48px -48px; +} + +.icon-bookmark { + background-position: -72px -48px; +} + +.icon-print { + background-position: -96px -48px; +} + +.icon-camera { + background-position: -120px -48px; +} + +.icon-font { + background-position: -144px -48px; +} + +.icon-bold { + background-position: -167px -48px; +} + +.icon-italic { + background-position: -192px -48px; +} + +.icon-text-height { + background-position: -216px -48px; +} + +.icon-text-width { + background-position: -240px -48px; +} + +.icon-align-left { + background-position: -264px -48px; +} + +.icon-align-center { + background-position: -288px -48px; +} + +.icon-align-right { + background-position: -312px -48px; +} + +.icon-align-justify { + background-position: -336px -48px; +} + +.icon-list { + background-position: -360px -48px; +} + +.icon-indent-left { + background-position: -384px -48px; +} + +.icon-indent-right { + background-position: -408px -48px; +} + +.icon-facetime-video { + background-position: -432px -48px; +} + +.icon-picture { + background-position: -456px -48px; +} + +.icon-pencil { + background-position: 0 -72px; +} + +.icon-map-marker { + background-position: -24px -72px; +} + +.icon-adjust { + background-position: -48px -72px; +} + +.icon-tint { + background-position: -72px -72px; +} + +.icon-edit { + background-position: -96px -72px; +} + +.icon-share { + background-position: -120px -72px; +} + +.icon-check { + background-position: -144px -72px; +} + +.icon-move { + background-position: -168px -72px; +} + +.icon-step-backward { + background-position: -192px -72px; +} + +.icon-fast-backward { + background-position: -216px -72px; +} + +.icon-backward { + background-position: -240px -72px; +} + +.icon-play { + background-position: -264px -72px; +} + +.icon-pause { + background-position: -288px -72px; +} + +.icon-stop { + background-position: -312px -72px; +} + +.icon-forward { + background-position: -336px -72px; +} + +.icon-fast-forward { + background-position: -360px -72px; +} + +.icon-step-forward { + background-position: -384px -72px; +} + +.icon-eject { + background-position: -408px -72px; +} + +.icon-chevron-left { + background-position: -432px -72px; +} + +.icon-chevron-right { + background-position: -456px -72px; +} + +.icon-plus-sign { + background-position: 0 -96px; +} + +.icon-minus-sign { + background-position: -24px -96px; +} + +.icon-remove-sign { + background-position: -48px -96px; +} + +.icon-ok-sign { + background-position: -72px -96px; +} + +.icon-question-sign { + background-position: -96px -96px; +} + +.icon-info-sign { + background-position: -120px -96px; +} + +.icon-screenshot { + background-position: -144px -96px; +} + +.icon-remove-circle { + background-position: -168px -96px; +} + +.icon-ok-circle { + background-position: -192px -96px; +} + +.icon-ban-circle { + background-position: -216px -96px; +} + +.icon-arrow-left { + background-position: -240px -96px; +} + +.icon-arrow-right { + background-position: -264px -96px; +} + +.icon-arrow-up { + background-position: -289px -96px; +} + +.icon-arrow-down { + background-position: -312px -96px; +} + +.icon-share-alt { + background-position: -336px -96px; +} + +.icon-resize-full { + background-position: -360px -96px; +} + +.icon-resize-small { + background-position: -384px -96px; +} + +.icon-plus { + background-position: -408px -96px; +} + +.icon-minus { + background-position: -433px -96px; +} + +.icon-asterisk { + background-position: -456px -96px; +} + +.icon-exclamation-sign { + background-position: 0 -120px; +} + +.icon-gift { + background-position: -24px -120px; +} + +.icon-leaf { + background-position: -48px -120px; +} + +.icon-fire { + background-position: -72px -120px; +} + +.icon-eye-open { + background-position: -96px -120px; +} + +.icon-eye-close { + background-position: -120px -120px; +} + +.icon-warning-sign { + background-position: -144px -120px; +} + +.icon-plane { + background-position: -168px -120px; +} + +.icon-calendar { + background-position: -192px -120px; +} + +.icon-random { + width: 16px; + background-position: -216px -120px; +} + +.icon-comment { + background-position: -240px -120px; +} + +.icon-magnet { + background-position: -264px -120px; +} + +.icon-chevron-up { + background-position: -288px -120px; +} + +.icon-chevron-down { + background-position: -313px -119px; +} + +.icon-retweet { + background-position: -336px -120px; +} + +.icon-shopping-cart { + background-position: -360px -120px; +} + +.icon-folder-close { + width: 16px; + background-position: -384px -120px; +} + +.icon-folder-open { + width: 16px; + background-position: -408px -120px; +} + +.icon-resize-vertical { + background-position: -432px -119px; +} + +.icon-resize-horizontal { + background-position: -456px -118px; +} + +.icon-hdd { + background-position: 0 -144px; +} + +.icon-bullhorn { + background-position: -24px -144px; +} + +.icon-bell { + background-position: -48px -144px; +} + +.icon-certificate { + background-position: -72px -144px; +} + +.icon-thumbs-up { + background-position: -96px -144px; +} + +.icon-thumbs-down { + background-position: -120px -144px; +} + +.icon-hand-right { + background-position: -144px -144px; +} + +.icon-hand-left { + background-position: -168px -144px; +} + +.icon-hand-up { + background-position: -192px -144px; +} + +.icon-hand-down { + background-position: -216px -144px; +} + +.icon-circle-arrow-right { + background-position: -240px -144px; +} + +.icon-circle-arrow-left { + background-position: -264px -144px; +} + +.icon-circle-arrow-up { + background-position: -288px -144px; +} + +.icon-circle-arrow-down { + background-position: -312px -144px; +} + +.icon-globe { + background-position: -336px -144px; +} + +.icon-wrench { + background-position: -360px -144px; +} + +.icon-tasks { + background-position: -384px -144px; +} + +.icon-filter { + background-position: -408px -144px; +} + +.icon-briefcase { + background-position: -432px -144px; +} + +.icon-fullscreen { + background-position: -456px -144px; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle { + *margin-bottom: -3px; +} + +.dropdown-toggle:active, +.open .dropdown-toggle { + outline: 0; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus, +.dropdown-submenu:hover > a, +.dropdown-submenu:focus > a { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + outline: 0; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #999999; +} + +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: default; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.open { + *z-index: 1000; +} + +.open > .dropdown-menu { + display: block; +} + +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid #000000; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +.dropdown-submenu { + position: relative; +} + +.dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + -webkit-border-radius: 0 6px 6px 6px; + -moz-border-radius: 0 6px 6px 6px; + border-radius: 0 6px 6px 6px; +} + +.dropdown-submenu:hover > .dropdown-menu { + display: block; +} + +.dropup .dropdown-submenu > .dropdown-menu { + top: auto; + bottom: 0; + margin-top: 0; + margin-bottom: -2px; + -webkit-border-radius: 5px 5px 5px 0; + -moz-border-radius: 5px 5px 5px 0; + border-radius: 5px 5px 5px 0; +} + +.dropdown-submenu > a:after { + display: block; + float: right; + width: 0; + height: 0; + margin-top: 5px; + margin-right: -10px; + border-color: transparent; + border-left-color: #cccccc; + border-style: solid; + border-width: 5px 0 5px 5px; + content: " "; +} + +.dropdown-submenu:hover > a:after { + border-left-color: #ffffff; +} + +.dropdown-submenu.pull-left { + float: none; +} + +.dropdown-submenu.pull-left > .dropdown-menu { + left: -100%; + margin-left: 10px; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.dropdown .dropdown-menu .nav-header { + padding-right: 20px; + padding-left: 20px; +} + +.typeahead { + z-index: 1051; + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-large { + padding: 24px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.well-small { + padding: 9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +.collapse.in { + height: auto; +} + +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 20px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover, +.close:focus { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; + filter: alpha(opacity=40); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.btn { + display: inline-block; + *display: inline; + padding: 4px 12px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 14px; + line-height: 20px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + cursor: pointer; + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border: 1px solid #cccccc; + *border: 0; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-bottom-color: #b3b3b3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:focus, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + color: #333333; + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover, +.btn:focus { + color: #333333; + text-decoration: none; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn.disabled, +.btn[disabled] { + cursor: default; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-large { + padding: 11px 19px; + font-size: 17.5px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.btn-large [class^="icon-"], +.btn-large [class*=" icon-"] { + margin-top: 4px; +} + +.btn-small { + padding: 2px 10px; + font-size: 11.9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-small [class^="icon-"], +.btn-small [class*=" icon-"] { + margin-top: 0; +} + +.btn-mini [class^="icon-"], +.btn-mini [class*=" icon-"] { + margin-top: -1px; +} + +.btn-mini { + padding: 0 6px; + font-size: 10.5px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} + +.btn-primary { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #006dcc; + *background-color: #0044cc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(to bottom, #0088cc, #0044cc); + background-repeat: repeat-x; + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + color: #ffffff; + background-color: #0044cc; + *background-color: #003bb3; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #003399 \9; +} + +.btn-warning { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #faa732; + *background-color: #f89406; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-warning:hover, +.btn-warning:focus, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + color: #ffffff; + background-color: #f89406; + *background-color: #df8505; +} + +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} + +.btn-danger { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #da4f49; + *background-color: #bd362f; + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); + background-repeat: repeat-x; + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-danger:hover, +.btn-danger:focus, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + color: #ffffff; + background-color: #bd362f; + *background-color: #a9302a; +} + +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} + +.btn-success { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #5bb75b; + *background-color: #51a351; + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(to bottom, #62c462, #51a351); + background-repeat: repeat-x; + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-success:hover, +.btn-success:focus, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + color: #ffffff; + background-color: #51a351; + *background-color: #499249; +} + +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} + +.btn-info { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #49afcd; + *background-color: #2f96b4; + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); + background-repeat: repeat-x; + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-info:hover, +.btn-info:focus, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + color: #ffffff; + background-color: #2f96b4; + *background-color: #2a85a0; +} + +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} + +.btn-inverse { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #363636; + *background-color: #222222; + background-image: -moz-linear-gradient(top, #444444, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); + background-image: -webkit-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(top, #444444, #222222); + background-image: linear-gradient(to bottom, #444444, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-inverse:hover, +.btn-inverse:focus, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + color: #ffffff; + background-color: #222222; + *background-color: #151515; +} + +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} + +button.btn, +input[type="submit"].btn { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} + +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} + +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} + +.btn-link, +.btn-link:active, +.btn-link[disabled] { + background-color: transparent; + background-image: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-link { + color: #0088cc; + cursor: pointer; + border-color: transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-link:hover, +.btn-link:focus { + color: #005580; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover, +.btn-link[disabled]:focus { + color: #333333; + text-decoration: none; +} + +.btn-group { + position: relative; + display: inline-block; + *display: inline; + *margin-left: .3em; + font-size: 0; + white-space: nowrap; + vertical-align: middle; + *zoom: 1; +} + +.btn-group:first-child { + *margin-left: 0; +} + +.btn-group + .btn-group { + margin-left: 5px; +} + +.btn-toolbar { + margin-top: 10px; + margin-bottom: 10px; + font-size: 0; +} + +.btn-toolbar > .btn + .btn, +.btn-toolbar > .btn-group + .btn, +.btn-toolbar > .btn + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn { + position: relative; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group > .btn + .btn { + margin-left: -1px; +} + +.btn-group > .btn, +.btn-group > .dropdown-menu, +.btn-group > .popover { + font-size: 14px; +} + +.btn-group > .btn-mini { + font-size: 10.5px; +} + +.btn-group > .btn-small { + font-size: 11.9px; +} + +.btn-group > .btn-large { + font-size: 17.5px; +} + +.btn-group > .btn:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.btn-group > .btn.large:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.btn-group > .btn.large:last-child, +.btn-group > .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active { + z-index: 2; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + *padding-top: 5px; + padding-right: 8px; + *padding-bottom: 5px; + padding-left: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group > .btn-mini + .dropdown-toggle { + *padding-top: 2px; + padding-right: 5px; + *padding-bottom: 2px; + padding-left: 5px; +} + +.btn-group > .btn-small + .dropdown-toggle { + *padding-top: 5px; + *padding-bottom: 4px; +} + +.btn-group > .btn-large + .dropdown-toggle { + *padding-top: 7px; + padding-right: 12px; + *padding-bottom: 7px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group.open .btn.dropdown-toggle { + background-color: #e6e6e6; +} + +.btn-group.open .btn-primary.dropdown-toggle { + background-color: #0044cc; +} + +.btn-group.open .btn-warning.dropdown-toggle { + background-color: #f89406; +} + +.btn-group.open .btn-danger.dropdown-toggle { + background-color: #bd362f; +} + +.btn-group.open .btn-success.dropdown-toggle { + background-color: #51a351; +} + +.btn-group.open .btn-info.dropdown-toggle { + background-color: #2f96b4; +} + +.btn-group.open .btn-inverse.dropdown-toggle { + background-color: #222222; +} + +.btn .caret { + margin-top: 8px; + margin-left: 0; +} + +.btn-large .caret { + margin-top: 6px; +} + +.btn-large .caret { + border-top-width: 5px; + border-right-width: 5px; + border-left-width: 5px; +} + +.btn-mini .caret, +.btn-small .caret { + margin-top: 8px; +} + +.dropup .btn-large .caret { + border-bottom-width: 5px; +} + +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.btn-group-vertical { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-group-vertical > .btn { + display: block; + float: none; + max-width: 100%; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group-vertical > .btn + .btn { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:first-child { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.btn-group-vertical > .btn:last-child { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.btn-group-vertical > .btn-large:first-child { + -webkit-border-radius: 6px 6px 0 0; + -moz-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; +} + +.btn-group-vertical > .btn-large:last-child { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 20px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.alert, +.alert h4 { + color: #c09853; +} + +.alert h4 { + margin: 0; +} + +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 20px; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-success h4 { + color: #468847; +} + +.alert-danger, +.alert-error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-danger h4, +.alert-error h4 { + color: #b94a48; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-info h4 { + color: #3a87ad; +} + +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} + +.alert-block > p, +.alert-block > ul { + margin-bottom: 0; +} + +.alert-block p + p { + margin-top: 5px; +} + +.nav { + margin-bottom: 20px; + margin-left: 0; + list-style: none; +} + +.nav > li > a { + display: block; +} + +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > li > a > img { + max-width: none; +} + +.nav > .pull-right { + float: right; +} + +.nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.nav li + .nav-header { + margin-top: 9px; +} + +.nav-list { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 0; +} + +.nav-list > li > a, +.nav-list .nav-header { + margin-right: -15px; + margin-left: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} + +.nav-list > li > a { + padding: 3px 15px; +} + +.nav-list > .active > a, +.nav-list > .active > a:hover, +.nav-list > .active > a:focus { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} + +.nav-list [class^="icon-"], +.nav-list [class*=" icon-"] { + margin-right: 2px; +} + +.nav-list .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.nav-tabs, +.nav-pills { + *zoom: 1; +} + +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + line-height: 0; + content: ""; +} + +.nav-tabs:after, +.nav-pills:after { + clear: both; +} + +.nav-tabs > li, +.nav-pills > li { + float: left; +} + +.nav-tabs > li > a, +.nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs > li { + margin-bottom: -1px; +} + +.nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 20px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover, +.nav-tabs > li > a:focus { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover, +.nav-tabs > .active > a:focus { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} + +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.nav-pills > .active > a, +.nav-pills > .active > a:hover, +.nav-pills > .active > a:focus { + color: #ffffff; + background-color: #0088cc; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li > a { + margin-right: 0; +} + +.nav-tabs.nav-stacked { + border-bottom: 0; +} + +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-topleft: 4px; +} + +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.nav-tabs.nav-stacked > li > a:hover, +.nav-tabs.nav-stacked > li > a:focus { + z-index: 2; + border-color: #ddd; +} + +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} + +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} + +.nav-tabs .dropdown-menu { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.nav-pills .dropdown-menu { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.nav .dropdown-toggle .caret { + margin-top: 6px; + border-top-color: #0088cc; + border-bottom-color: #0088cc; +} + +.nav .dropdown-toggle:hover .caret, +.nav .dropdown-toggle:focus .caret { + border-top-color: #005580; + border-bottom-color: #005580; +} + +/* move down carets for tabs */ + +.nav-tabs .dropdown-toggle .caret { + margin-top: 8px; +} + +.nav .active .dropdown-toggle .caret { + border-top-color: #fff; + border-bottom-color: #fff; +} + +.nav-tabs .active .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.nav > .dropdown.active > a:hover, +.nav > .dropdown.active > a:focus { + cursor: pointer; +} + +.nav-tabs .open .dropdown-toggle, +.nav-pills .open .dropdown-toggle, +.nav > li.dropdown.open.active > a:hover, +.nav > li.dropdown.open.active > a:focus { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} + +.nav li.dropdown.open .caret, +.nav li.dropdown.open.active .caret, +.nav li.dropdown.open a:hover .caret, +.nav li.dropdown.open a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} + +.tabs-stacked .open > a:hover, +.tabs-stacked .open > a:focus { + border-color: #999999; +} + +.tabbable { + *zoom: 1; +} + +.tabbable:before, +.tabbable:after { + display: table; + line-height: 0; + content: ""; +} + +.tabbable:after { + clear: both; +} + +.tab-content { + overflow: auto; +} + +.tabs-below > .nav-tabs, +.tabs-right > .nav-tabs, +.tabs-left > .nav-tabs { + border-bottom: 0; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.tabs-below > .nav-tabs { + border-top: 1px solid #ddd; +} + +.tabs-below > .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} + +.tabs-below > .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.tabs-below > .nav-tabs > li > a:hover, +.tabs-below > .nav-tabs > li > a:focus { + border-top-color: #ddd; + border-bottom-color: transparent; +} + +.tabs-below > .nav-tabs > .active > a, +.tabs-below > .nav-tabs > .active > a:hover, +.tabs-below > .nav-tabs > .active > a:focus { + border-color: transparent #ddd #ddd #ddd; +} + +.tabs-left > .nav-tabs > li, +.tabs-right > .nav-tabs > li { + float: none; +} + +.tabs-left > .nav-tabs > li > a, +.tabs-right > .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} + +.tabs-left > .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} + +.tabs-left > .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.tabs-left > .nav-tabs > li > a:hover, +.tabs-left > .nav-tabs > li > a:focus { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} + +.tabs-left > .nav-tabs .active > a, +.tabs-left > .nav-tabs .active > a:hover, +.tabs-left > .nav-tabs .active > a:focus { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} + +.tabs-right > .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} + +.tabs-right > .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.tabs-right > .nav-tabs > li > a:hover, +.tabs-right > .nav-tabs > li > a:focus { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} + +.tabs-right > .nav-tabs .active > a, +.tabs-right > .nav-tabs .active > a:hover, +.tabs-right > .nav-tabs .active > a:focus { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} + +.nav > .disabled > a { + color: #999999; +} + +.nav > .disabled > a:hover, +.nav > .disabled > a:focus { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.navbar { + *position: relative; + *z-index: 2; + margin-bottom: 20px; + overflow: visible; +} + +.navbar-inner { + min-height: 40px; + padding-right: 20px; + padding-left: 20px; + background-color: #fafafa; + background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); + background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); + background-repeat: repeat-x; + border: 1px solid #d4d4d4; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); + *zoom: 1; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); +} + +.navbar-inner:before, +.navbar-inner:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-inner:after { + clear: both; +} + +.navbar .container { + width: auto; +} + +.nav-collapse.collapse { + height: auto; + overflow: visible; +} + +.navbar .brand { + display: block; + float: left; + padding: 10px 20px 10px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + color: #777777; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .brand:hover, +.navbar .brand:focus { + text-decoration: none; +} + +.navbar-text { + margin-bottom: 0; + line-height: 40px; + color: #777777; +} + +.navbar-link { + color: #777777; +} + +.navbar-link:hover, +.navbar-link:focus { + color: #333333; +} + +.navbar .divider-vertical { + height: 40px; + margin: 0 9px; + border-right: 1px solid #ffffff; + border-left: 1px solid #f2f2f2; +} + +.navbar .btn, +.navbar .btn-group { + margin-top: 5px; +} + +.navbar .btn-group .btn, +.navbar .input-prepend .btn, +.navbar .input-append .btn, +.navbar .input-prepend .btn-group, +.navbar .input-append .btn-group { + margin-top: 0; +} + +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} + +.navbar-form:before, +.navbar-form:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-form:after { + clear: both; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .radio, +.navbar-form .checkbox { + margin-top: 5px; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .btn { + display: inline-block; + margin-bottom: 0; +} + +.navbar-form input[type="image"], +.navbar-form input[type="checkbox"], +.navbar-form input[type="radio"] { + margin-top: 3px; +} + +.navbar-form .input-append, +.navbar-form .input-prepend { + margin-top: 5px; + white-space: nowrap; +} + +.navbar-form .input-append input, +.navbar-form .input-prepend input { + margin-top: 0; +} + +.navbar-search { + position: relative; + float: left; + margin-top: 5px; + margin-bottom: 0; +} + +.navbar-search .search-query { + padding: 4px 14px; + margin-bottom: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.navbar-static-top { + position: static; + margin-bottom: 0; +} + +.navbar-static-top .navbar-inner { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; + margin-bottom: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + border-width: 0 0 1px; +} + +.navbar-fixed-bottom .navbar-inner { + border-width: 1px 0 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner { + padding-right: 0; + padding-left: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar-fixed-bottom .navbar-inner { + -webkit-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} + +.navbar .nav.pull-right { + float: right; + margin-right: 0; +} + +.navbar .nav > li { + float: left; +} + +.navbar .nav > li > a { + float: none; + padding: 10px 15px 10px; + color: #777777; + text-decoration: none; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .nav .dropdown-toggle .caret { + margin-top: 8px; +} + +.navbar .nav > li > a:focus, +.navbar .nav > li > a:hover { + color: #333333; + text-decoration: none; + background-color: transparent; +} + +.navbar .nav > .active > a, +.navbar .nav > .active > a:hover, +.navbar .nav > .active > a:focus { + color: #555555; + text-decoration: none; + background-color: #e5e5e5; + -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); +} + +.navbar .btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-right: 5px; + margin-left: 5px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #ededed; + *background-color: #e5e5e5; + background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); + background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); + background-repeat: repeat-x; + border-color: #e5e5e5 #e5e5e5 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} + +.navbar .btn-navbar:hover, +.navbar .btn-navbar:focus, +.navbar .btn-navbar:active, +.navbar .btn-navbar.active, +.navbar .btn-navbar.disabled, +.navbar .btn-navbar[disabled] { + color: #ffffff; + background-color: #e5e5e5; + *background-color: #d9d9d9; +} + +.navbar .btn-navbar:active, +.navbar .btn-navbar.active { + background-color: #cccccc \9; +} + +.navbar .btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} + +.navbar .nav > li > .dropdown-menu:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.navbar .nav > li > .dropdown-menu:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:before { + top: auto; + bottom: -7px; + border-top: 7px solid #ccc; + border-bottom: 0; + border-top-color: rgba(0, 0, 0, 0.2); +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:after { + top: auto; + bottom: -6px; + border-top: 6px solid #ffffff; + border-bottom: 0; +} + +.navbar .nav li.dropdown > a:hover .caret, +.navbar .nav li.dropdown > a:focus .caret { + border-top-color: #333333; + border-bottom-color: #333333; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle, +.navbar .nav li.dropdown.active > .dropdown-toggle, +.navbar .nav li.dropdown.open.active > .dropdown-toggle { + color: #555555; + background-color: #e5e5e5; +} + +.navbar .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #777777; + border-bottom-color: #777777; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar .pull-right > li > .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:before, +.navbar .nav > li > .dropdown-menu.pull-right:before { + right: 12px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:after, +.navbar .nav > li > .dropdown-menu.pull-right:after { + right: 13px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { + right: 100%; + left: auto; + margin-right: -1px; + margin-left: 0; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.navbar-inverse .navbar-inner { + background-color: #1b1b1b; + background-image: -moz-linear-gradient(top, #222222, #111111); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); + background-image: -webkit-linear-gradient(top, #222222, #111111); + background-image: -o-linear-gradient(top, #222222, #111111); + background-image: linear-gradient(to bottom, #222222, #111111); + background-repeat: repeat-x; + border-color: #252525; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); +} + +.navbar-inverse .brand, +.navbar-inverse .nav > li > a { + color: #999999; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.navbar-inverse .brand:hover, +.navbar-inverse .nav > li > a:hover, +.navbar-inverse .brand:focus, +.navbar-inverse .nav > li > a:focus { + color: #ffffff; +} + +.navbar-inverse .brand { + color: #999999; +} + +.navbar-inverse .navbar-text { + color: #999999; +} + +.navbar-inverse .nav > li > a:focus, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .nav .active > a, +.navbar-inverse .nav .active > a:hover, +.navbar-inverse .nav .active > a:focus { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover, +.navbar-inverse .navbar-link:focus { + color: #ffffff; +} + +.navbar-inverse .divider-vertical { + border-right-color: #222222; + border-left-color: #111111; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .nav li.dropdown > a:hover .caret, +.navbar-inverse .nav li.dropdown > a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-search .search-query { + color: #ffffff; + background-color: #515151; + border-color: #111111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; +} + +.navbar-inverse .navbar-search .search-query:-moz-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:focus, +.navbar-inverse .navbar-search .search-query.focused { + padding: 5px 15px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + outline: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); +} + +.navbar-inverse .btn-navbar { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e0e0e; + *background-color: #040404; + background-image: -moz-linear-gradient(top, #151515, #040404); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); + background-image: -webkit-linear-gradient(top, #151515, #040404); + background-image: -o-linear-gradient(top, #151515, #040404); + background-image: linear-gradient(to bottom, #151515, #040404); + background-repeat: repeat-x; + border-color: #040404 #040404 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.navbar-inverse .btn-navbar:hover, +.navbar-inverse .btn-navbar:focus, +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active, +.navbar-inverse .btn-navbar.disabled, +.navbar-inverse .btn-navbar[disabled] { + color: #ffffff; + background-color: #040404; + *background-color: #000000; +} + +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active { + background-color: #000000 \9; +} + +.breadcrumb { + padding: 8px 15px; + margin: 0 0 20px; + list-style: none; + background-color: #f5f5f5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.breadcrumb > li { + display: inline-block; + *display: inline; + text-shadow: 0 1px 0 #ffffff; + *zoom: 1; +} + +.breadcrumb > li > .divider { + padding: 0 5px; + color: #ccc; +} + +.breadcrumb > .active { + color: #999999; +} + +.pagination { + margin: 20px 0; +} + +.pagination ul { + display: inline-block; + *display: inline; + margin-bottom: 0; + margin-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + *zoom: 1; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.pagination ul > li { + display: inline; +} + +.pagination ul > li > a, +.pagination ul > li > span { + float: left; + padding: 4px 12px; + line-height: 20px; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; + border-left-width: 0; +} + +.pagination ul > li > a:hover, +.pagination ul > li > a:focus, +.pagination ul > .active > a, +.pagination ul > .active > span { + background-color: #f5f5f5; +} + +.pagination ul > .active > a, +.pagination ul > .active > span { + color: #999999; + cursor: default; +} + +.pagination ul > .disabled > span, +.pagination ul > .disabled > a, +.pagination ul > .disabled > a:hover, +.pagination ul > .disabled > a:focus { + color: #999999; + cursor: default; + background-color: transparent; +} + +.pagination ul > li:first-child > a, +.pagination ul > li:first-child > span { + border-left-width: 1px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.pagination ul > li:last-child > a, +.pagination ul > li:last-child > span { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.pagination-centered { + text-align: center; +} + +.pagination-right { + text-align: right; +} + +.pagination-large ul > li > a, +.pagination-large ul > li > span { + padding: 11px 19px; + font-size: 17.5px; +} + +.pagination-large ul > li:first-child > a, +.pagination-large ul > li:first-child > span { + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.pagination-large ul > li:last-child > a, +.pagination-large ul > li:last-child > span { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.pagination-mini ul > li:first-child > a, +.pagination-small ul > li:first-child > a, +.pagination-mini ul > li:first-child > span, +.pagination-small ul > li:first-child > span { + -webkit-border-bottom-left-radius: 3px; + border-bottom-left-radius: 3px; + -webkit-border-top-left-radius: 3px; + border-top-left-radius: 3px; + -moz-border-radius-bottomleft: 3px; + -moz-border-radius-topleft: 3px; +} + +.pagination-mini ul > li:last-child > a, +.pagination-small ul > li:last-child > a, +.pagination-mini ul > li:last-child > span, +.pagination-small ul > li:last-child > span { + -webkit-border-top-right-radius: 3px; + border-top-right-radius: 3px; + -webkit-border-bottom-right-radius: 3px; + border-bottom-right-radius: 3px; + -moz-border-radius-topright: 3px; + -moz-border-radius-bottomright: 3px; +} + +.pagination-small ul > li > a, +.pagination-small ul > li > span { + padding: 2px 10px; + font-size: 11.9px; +} + +.pagination-mini ul > li > a, +.pagination-mini ul > li > span { + padding: 0 6px; + font-size: 10.5px; +} + +.pager { + margin: 20px 0; + text-align: center; + list-style: none; + *zoom: 1; +} + +.pager:before, +.pager:after { + display: table; + line-height: 0; + content: ""; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #f5f5f5; +} + +.pager .next > a, +.pager .next > span { + float: right; +} + +.pager .previous > a, +.pager .previous > span { + float: left; +} + +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #999999; + cursor: default; + background-color: #fff; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.modal { + position: fixed; + top: 10%; + left: 50%; + z-index: 1050; + width: 560px; + margin-left: -280px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + outline: none; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} + +.modal.fade { + top: -25%; + -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; + -moz-transition: opacity 0.3s linear, top 0.3s ease-out; + -o-transition: opacity 0.3s linear, top 0.3s ease-out; + transition: opacity 0.3s linear, top 0.3s ease-out; +} + +.modal.fade.in { + top: 10%; +} + +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} + +.modal-header .close { + margin-top: 2px; +} + +.modal-header h3 { + margin: 0; + line-height: 30px; +} + +.modal-body { + position: relative; + max-height: 400px; + padding: 15px; + overflow-y: auto; +} + +.modal-form { + margin-bottom: 0; +} + +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + line-height: 0; + content: ""; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + font-size: 11px; + line-height: 1.4; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} + +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} + +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} + +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + white-space: normal; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.popover.top { + margin-top: -10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-left: -10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.popover-title:empty { + display: none; +} + +.popover-content { + padding: 9px 14px; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow { + border-width: 11px; +} + +.popover .arrow:after { + border-width: 10px; + content: ""; +} + +.popover.top .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.top .arrow:after { + bottom: 1px; + margin-left: -10px; + border-top-color: #ffffff; + border-bottom-width: 0; +} + +.popover.right .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.right .arrow:after { + bottom: -10px; + left: 1px; + border-right-color: #ffffff; + border-left-width: 0; +} + +.popover.bottom .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-top-width: 0; +} + +.popover.bottom .arrow:after { + top: 1px; + margin-left: -10px; + border-bottom-color: #ffffff; + border-top-width: 0; +} + +.popover.left .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, 0.25); + border-right-width: 0; +} + +.popover.left .arrow:after { + right: 1px; + bottom: -10px; + border-left-color: #ffffff; + border-right-width: 0; +} + +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} + +.thumbnails:before, +.thumbnails:after { + display: table; + line-height: 0; + content: ""; +} + +.thumbnails:after { + clear: both; +} + +.row-fluid .thumbnails { + margin-left: 0; +} + +.thumbnails > li { + float: left; + margin-bottom: 20px; + margin-left: 20px; +} + +.thumbnail { + display: block; + padding: 4px; + line-height: 20px; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +a.thumbnail:hover, +a.thumbnail:focus { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} + +.thumbnail > img { + display: block; + max-width: 100%; + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #555555; +} + +.media, +.media-body { + overflow: hidden; + *overflow: visible; + zoom: 1; +} + +.media, +.media .media { + margin-top: 15px; +} + +.media:first-child { + margin-top: 0; +} + +.media-object { + display: block; +} + +.media-heading { + margin: 0 0 5px; +} + +.media > .pull-left { + margin-right: 10px; +} + +.media > .pull-right { + margin-left: 10px; +} + +.media-list { + margin-left: 0; + list-style: none; +} + +.label, +.badge { + display: inline-block; + padding: 2px 4px; + font-size: 11.844px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +.label { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.badge { + padding-right: 9px; + padding-left: 9px; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; +} + +.label:empty, +.badge:empty { + display: none; +} + +a.label:hover, +a.label:focus, +a.badge:hover, +a.badge:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label-important, +.badge-important { + background-color: #b94a48; +} + +.label-important[href], +.badge-important[href] { + background-color: #953b39; +} + +.label-warning, +.badge-warning { + background-color: #f89406; +} + +.label-warning[href], +.badge-warning[href] { + background-color: #c67605; +} + +.label-success, +.badge-success { + background-color: #468847; +} + +.label-success[href], +.badge-success[href] { + background-color: #356635; +} + +.label-info, +.badge-info { + background-color: #3a87ad; +} + +.label-info[href], +.badge-info[href] { + background-color: #2d6987; +} + +.label-inverse, +.badge-inverse { + background-color: #333333; +} + +.label-inverse[href], +.badge-inverse[href] { + background-color: #1a1a1a; +} + +.btn .label, +.btn .badge { + position: relative; + top: -1px; +} + +.btn-mini .label, +.btn-mini .badge { + top: 0; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-ms-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress .bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(to bottom, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress .bar + .bar { + -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); +} + +.progress-striped .bar { + background-color: #149bdf; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} + +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-danger .bar, +.progress .bar-danger { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); +} + +.progress-danger.progress-striped .bar, +.progress-striped .bar-danger { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-success .bar, +.progress .bar-success { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(to bottom, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); +} + +.progress-success.progress-striped .bar, +.progress-striped .bar-success { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-info .bar, +.progress .bar-info { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(to bottom, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); +} + +.progress-info.progress-striped .bar, +.progress-striped .bar-info { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-warning .bar, +.progress .bar-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); +} + +.progress-warning.progress-striped .bar, +.progress-striped .bar-warning { + background-color: #fbb450; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.accordion { + margin-bottom: 20px; +} + +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.accordion-heading { + border-bottom: 0; +} + +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} + +.accordion-toggle { + cursor: pointer; +} + +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} + +.carousel { + position: relative; + margin-bottom: 20px; + line-height: 1; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + line-height: 1; +} + +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.right { + right: 15px; + left: auto; +} + +.carousel-control:hover, +.carousel-control:focus { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-indicators { + position: absolute; + top: 15px; + right: 15px; + z-index: 5; + margin: 0; + list-style: none; +} + +.carousel-indicators li { + display: block; + float: left; + width: 10px; + height: 10px; + margin-left: 5px; + text-indent: -999px; + background-color: #ccc; + background-color: rgba(255, 255, 255, 0.25); + border-radius: 5px; +} + +.carousel-indicators .active { + background-color: #fff; +} + +.carousel-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 15px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} + +.carousel-caption h4, +.carousel-caption p { + line-height: 20px; + color: #ffffff; +} + +.carousel-caption h4 { + margin: 0 0 5px; +} + +.carousel-caption p { + margin-bottom: 0; +} + +.hero-unit { + padding: 60px; + margin-bottom: 30px; + font-size: 18px; + font-weight: 200; + line-height: 30px; + color: inherit; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; + color: inherit; +} + +.hero-unit li { + line-height: 30px; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.invisible { + visibility: hidden; +} + +.affix { + position: fixed; +} diff --git a/docs/_static/bootstrap-2.3.2/css/bootstrap.min.css b/docs/_static/bootstrap-2.3.2/css/bootstrap.min.css new file mode 100644 index 00000000..b6428e69 --- /dev/null +++ b/docs/_static/bootstrap-2.3.2/css/bootstrap.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v2.3.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover,a:focus{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover,a.muted:focus{color:#808080}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#f5f5f5}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover,.navbar-link:focus{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings-white.png b/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings-white.png new file mode 100644 index 00000000..3bf6484a Binary files /dev/null and b/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings-white.png differ diff --git a/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings.png b/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings.png new file mode 100644 index 00000000..a9969993 Binary files /dev/null and b/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings.png differ diff --git a/docs/_static/bootstrap-2.3.2/js/bootstrap.js b/docs/_static/bootstrap-2.3.2/js/bootstrap.js new file mode 100644 index 00000000..638bb187 --- /dev/null +++ b/docs/_static/bootstrap-2.3.2/js/bootstrap.js @@ -0,0 +1,2287 @@ +/* =================================================== + * bootstrap-transition.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) + * ======================================================= */ + + $(function () { + + $.support.transition = (function () { + + var transitionEnd = (function () { + + var el = document.createElement('bootstrap') + , transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd' + , 'MozTransition' : 'transitionend' + , 'OTransition' : 'oTransitionEnd otransitionend' + , 'transition' : 'transitionend' + } + , name + + for (name in transEndEventNames){ + if (el.style[name] !== undefined) { + return transEndEventNames[name] + } + } + + }()) + + return transitionEnd && { + end: transitionEnd + } + + })() + + }) + +}(window.$jqTheme || window.jQuery); +/* ========================================================== + * bootstrap-alert.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent.trigger(e = $.Event('close')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent + .trigger('closed') + .remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + var old = $.fn.alert + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT NO CONFLICT + * ================= */ + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + /* ALERT DATA-API + * ============== */ + + $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) + +}(window.$jqTheme || window.jQuery); +/* ============================================================ + * bootstrap-button.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* BUTTON PUBLIC CLASS DEFINITION + * ============================== */ + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.button.defaults, options) + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + , $el = this.$element + , data = $el.data() + , val = $el.is('input') ? 'val' : 'html' + + state = state + 'Text' + data.resetText || $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d) + }, 0) + } + + Button.prototype.toggle = function () { + var $parent = this.$element.closest('[data-toggle="buttons-radio"]') + + $parent && $parent + .find('.active') + .removeClass('active') + + this.$element.toggleClass('active') + } + + + /* BUTTON PLUGIN DEFINITION + * ======================== */ + + var old = $.fn.button + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('button') + , options = typeof option == 'object' && option + if (!data) $this.data('button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.defaults = { + loadingText: 'loading...' + } + + $.fn.button.Constructor = Button + + + /* BUTTON NO CONFLICT + * ================== */ + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + /* BUTTON DATA-API + * =============== */ + + $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + }) + +}(window.$jqTheme || window.jQuery); +/* ========================================================== + * bootstrap-carousel.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CAROUSEL CLASS DEFINITION + * ========================= */ + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.prototype = { + + cycle: function (e) { + if (!e) this.paused = false + if (this.interval) clearInterval(this.interval); + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + return this + } + + , getActiveIndex: function () { + this.$active = this.$element.find('.item.active') + this.$items = this.$active.parent().children() + return this.$items.index(this.$active) + } + + , to: function (pos) { + var activeIndex = this.getActiveIndex() + , that = this + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) { + return this.$element.one('slid', function () { + that.to(pos) + }) + } + + if (activeIndex == pos) { + return this.pause().cycle() + } + + return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) + } + + , pause: function (e) { + if (!e) this.paused = true + if (this.$element.find('.next, .prev').length && $.support.transition.end) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + clearInterval(this.interval) + this.interval = null + return this + } + + , next: function () { + if (this.sliding) return + return this.slide('next') + } + + , prev: function () { + if (this.sliding) return + return this.slide('prev') + } + + , slide: function (type, next) { + var $active = this.$element.find('.item.active') + , $next = next || $active[type]() + , isCycling = this.interval + , direction = type == 'next' ? 'left' : 'right' + , fallback = type == 'next' ? 'first' : 'last' + , that = this + , e + + this.sliding = true + + isCycling && this.pause() + + $next = $next.length ? $next : this.$element.find('.item')[fallback]() + + e = $.Event('slide', { + relatedTarget: $next[0] + , direction: direction + }) + + if ($next.hasClass('active')) return + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + this.$element.one('slid', function () { + var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) + $nextIndicator && $nextIndicator.addClass('active') + }) + } + + if ($.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + this.$element.one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + } else { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } + + isCycling && this.cycle() + + return this + } + + } + + + /* CAROUSEL PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.carousel + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('carousel') + , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) + , action = typeof option == 'string' ? option : options.slide + if (!data) $this.data('carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + $.fn.carousel.defaults = { + interval: 5000 + , pause: 'hover' + } + + $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL NO CONFLICT + * ==================== */ + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + /* CAROUSEL DATA-API + * ================= */ + + $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , options = $.extend({}, $target.data(), $this.data()) + , slideIndex + + $target.carousel(options) + + if (slideIndex = $this.attr('data-slide-to')) { + $target.data('carousel').pause().to(slideIndex).cycle() + } + + e.preventDefault() + }) + +}(window.$jqTheme || window.jQuery); +/* ============================================================= + * bootstrap-collapse.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* COLLAPSE PUBLIC CLASS DEFINITION + * ================================ */ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.collapse.defaults, options) + + if (this.options.parent) { + this.$parent = $(this.options.parent) + } + + this.options.toggle && this.toggle() + } + + Collapse.prototype = { + + constructor: Collapse + + , dimension: function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + , show: function () { + var dimension + , scroll + , actives + , hasData + + if (this.transitioning || this.$element.hasClass('in')) return + + dimension = this.dimension() + scroll = $.camelCase(['scroll', dimension].join('-')) + actives = this.$parent && this.$parent.find('> .accordion-group > .in') + + if (actives && actives.length) { + hasData = actives.data('collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('collapse', null) + } + + this.$element[dimension](0) + this.transition('addClass', $.Event('show'), 'shown') + $.support.transition && this.$element[dimension](this.$element[0][scroll]) + } + + , hide: function () { + var dimension + if (this.transitioning || !this.$element.hasClass('in')) return + dimension = this.dimension() + this.reset(this.$element[dimension]()) + this.transition('removeClass', $.Event('hide'), 'hidden') + this.$element[dimension](0) + } + + , reset: function (size) { + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + [dimension](size || 'auto') + [0].offsetWidth + + this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') + + return this + } + + , transition: function (method, startEvent, completeEvent) { + var that = this + , complete = function () { + if (startEvent.type == 'show') that.reset() + that.transitioning = 0 + that.$element.trigger(completeEvent) + } + + this.$element.trigger(startEvent) + + if (startEvent.isDefaultPrevented()) return + + this.transitioning = 1 + + this.$element[method]('in') + + $.support.transition && this.$element.hasClass('collapse') ? + this.$element.one($.support.transition.end, complete) : + complete() + } + + , toggle: function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* COLLAPSE PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.collapse + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('collapse') + , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.defaults = { + toggle: true + } + + $.fn.collapse.Constructor = Collapse + + + /* COLLAPSE NO CONFLICT + * ==================== */ + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + /* COLLAPSE DATA-API + * ================= */ + + $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + , target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + , option = $(target).data('collapse') ? 'toggle' : $this.data() + $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + $(target).collapse(option) + }) + +}(window.$jqTheme || window.jQuery); +/* ============================================================ + * bootstrap-dropdown.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle=dropdown]' + , Dropdown = function (element) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function (e) { + var $this = $(this) + , $parent + , isActive + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement) { + // if mobile we we use a backdrop because click events don't delegate + $('