Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'mesh_inset/geom.py')
-rw-r--r--mesh_inset/geom.py719
1 files changed, 719 insertions, 0 deletions
diff --git a/mesh_inset/geom.py b/mesh_inset/geom.py
new file mode 100644
index 00000000..98059dcf
--- /dev/null
+++ b/mesh_inset/geom.py
@@ -0,0 +1,719 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <pep8 compliant>
+
+"""Geometry classes and operations.
+Also, vector file representation (Art).
+"""
+
+__author__ = "howard.trickey@gmail.com"
+
+import math
+
+# distances less than about DISTTOL will be considered
+# essentially zero
+DISTTOL = 1e-3
+INVDISTTOL = 1e3
+
+
+class Points(object):
+ """Container of points without duplication, each mapped to an int.
+
+ Points are either have dimension at least 2, maybe more.
+
+ Implementation:
+ In order to efficiently find duplicates, we quantize the points
+ to triples of ints and map from quantized triples to vertex
+ index.
+
+ Attributes:
+ pos: list of tuple of float - coordinates indexed by
+ vertex number
+ invmap: dict of (int, int, int) to int - quantized coordinates
+ to vertex number map
+ """
+
+ def __init__(self, initlist=[]):
+ self.pos = []
+ self.invmap = dict()
+ for p in initlist:
+ self.AddPoint(p)
+
+ @staticmethod
+ def Quantize(p):
+ """Quantize the float tuple into an int tuple.
+
+ Args:
+ p: tuple of float
+ Returns:
+ tuple of int - scaled by INVDISTTOL and rounded p
+ """
+
+ return tuple([int(round(v * INVDISTTOL)) for v in p])
+
+ def AddPoint(self, p):
+ """Add point p to the Points set and return vertex number.
+
+ If there is an existing point which quantizes the same,,
+ don't add a new one but instead return existing index.
+
+ Args:
+ p: tuple of float - coordinates (2-tuple or 3-tuple)
+ Returns:
+ int - the vertex number of added (or existing) point
+ """
+
+ qp = Points.Quantize(p)
+ if qp in self.invmap:
+ return self.invmap[qp]
+ else:
+ self.invmap[qp] = len(self.pos)
+ self.pos.append(p)
+ return len(self.pos) - 1
+
+ def AddPoints(self, points):
+ """Add another set of points to this set.
+
+ We need to return a mapping from indices
+ in the argument points space into indices
+ in this point space.
+
+ Args:
+ points: Points - to union into this set
+ Returns:
+ list of int: maps added indices to new ones
+ """
+
+ vmap = [0] * len(points.pos)
+ for i in range(len(points.pos)):
+ vmap[i] = self.AddPoint(points.pos[i])
+ return vmap
+
+ def AddZCoord(self, z):
+ """Change this in place to have a z coordinate, with value z.
+
+ Assumes the coordinates are currently 2d.
+
+ Args:
+ z: the value of the z coordinate to add
+ Side Effect:
+ self now has a z-coordinate added
+ """
+
+ assert(len(self.pos) == 0 or len(self.pos[0]) == 2)
+ newinvmap = dict()
+ for i, (x, y) in enumerate(self.pos):
+ newp = (x, y, z)
+ self.pos[i] = newp
+ newinvmap[self.Quantize(newp)] = i
+ self.invmap = newinvmap
+
+ def AddToZCoord(self, i, delta):
+ """Change the z-coordinate of point with index i to add delta.
+
+ Assumes the coordinates are currently 3d.
+
+ Args:
+ i: int - index of a point
+ delta: float - value to add to z-coord
+ """
+
+ (x, y, z) = self.pos[i]
+ self.pos[i] = (x, y, z + delta)
+
+
+class PolyArea(object):
+ """Contains a Polygonal Area (polygon with possible holes).
+
+ A polygon is a list of vertex ids, each an index given by
+ a Points object. The list represents a CCW-oriented
+ outer boundary (implicitly closed).
+ If there are holes, they are lists of CW-oriented vertices
+ that should be contained in the outer boundary.
+ (So the left face of both the poly and the holes is
+ the filled part.)
+
+ Attributes:
+ points: Points
+ poly: list of vertex ids
+ holes: list of lists of vertex ids (each a hole in poly)
+ data: any - application data (can hold color, e.g.)
+ """
+
+ def __init__(self, points=None, poly=None, holes=None, data=None):
+ self.points = points if points else Points()
+ self.poly = poly if poly else []
+ self.holes = holes if holes else []
+ self.data = data
+
+ def AddHole(self, holepa):
+ """Add a PolyArea's poly as a hole of self.
+
+ Need to reverse the contour and
+ adjust the the point indexes and self.points.
+
+ Args:
+ holepa: PolyArea
+ """
+
+ vmap = self.points.AddPoints(holepa.points)
+ holepoly = [vmap[i] for i in holepa.poly]
+ holepoly.reverse()
+ self.holes.append(holepoly)
+
+ def ContainsPoly(self, poly, points):
+ """Tests if poly is contained within self.poly.
+
+ Args:
+ poly: list of int - indices into points
+ points: Points - maps to coords
+ Returns:
+ bool - True if poly is fully contained within self.poly
+ """
+
+ for v in poly:
+ if PointInside(points.pos[v], self.poly, self.points) == -1:
+ return False
+ return True
+
+ def Normal(self):
+ """Returns the normal of the polyarea's main poly."""
+
+ pos = self.points.pos
+ poly = self.poly
+ if len(pos) == 0 or len(pos[0]) == 2 or len(poly) == 0:
+ print("whoops, not enough info to calculate normal")
+ return (0.0, 0.0, 1.0)
+ return Newell(poly, self.points)
+
+
+class PolyAreas(object):
+ """Contains a list of PolyAreas and a shared Points.
+
+ Attributes:
+ polyareas: list of PolyArea
+ points: Points
+ """
+
+ def __init__(self):
+ self.polyareas = []
+ self.points = Points()
+
+ def scale_and_center(self, scaled_side_target):
+ """Adjust the coordinates of the polyareas so that
+ it is centered at the origin and has its longest
+ dimension scaled to be scaled_side_target."""
+
+ if len(self.points.pos) == 0:
+ return
+ (minv, maxv) = self.bounds()
+ maxside = max([maxv[i] - minv[i] for i in range(2)])
+ if maxside > 0.0:
+ scale = scaled_side_target / maxside
+ else:
+ scale = 1.0
+ translate = [-0.5 * (maxv[i] + minv[i]) for i in range(2)]
+ dim = len(self.points.pos[0])
+ if dim == 3:
+ translate.append([0.0])
+ for v in range(len(self.points.pos)):
+ self.points.pos[v] = tuple([scale * (self.points.pos[v][i] + \
+ translate[i]) for i in range(dim)])
+
+ def bounds(self):
+ """Find bounding box of polyareas in xy.
+
+ Returns:
+ ([minx,miny],[maxx,maxy]) - all floats
+ """
+
+ huge = 1e100
+ minv = [huge, huge]
+ maxv = [-huge, -huge]
+ for pa in self.polyareas:
+ for face in [pa.poly] + pa.holes:
+ for v in face:
+ vcoords = self.points.pos[v]
+ for i in range(2):
+ if vcoords[i] < minv[i]:
+ minv[i] = vcoords[i]
+ if vcoords[i] > maxv[i]:
+ maxv[i] = vcoords[i]
+ if minv[0] == huge:
+ minv = [0.0, 0.0]
+ if maxv[0] == huge:
+ maxv = [0.0, 0.0]
+ return (minv, maxv)
+
+
+class Model(object):
+ """Contains a generic 3d model.
+
+ A generic 3d model has vertices with 3d coordinates.
+ Each vertex gets a 'vertex id', which is an index that
+ can be used to refer to the vertex and can be used
+ to retrieve the 3d coordinates of the point.
+
+ The actual visible part of the geometry are the faces,
+ which are n-gons (n>2), specified by a vector of the
+ n corner vertices.
+ Faces may also have data associated with them,
+ and the data will be copied into newly created faces
+ from the most likely neighbor faces..
+
+ Attributes:
+ points: geom.Points - the 3d vertices
+ faces: list of list of indices (each a CCW traversal of a face)
+ face_data: list of any - if present, is parallel to
+ faces list and holds arbitrary data
+ """
+
+ def __init__(self):
+ self.points = Points()
+ self.faces = []
+ self.face_data = []
+
+
+class Art(object):
+ """Contains a vector art diagram.
+
+ Attributes:
+ paths: list of Path objects
+ """
+
+ def __init__(self):
+ self.paths = []
+
+
+class Paint(object):
+ """A color or pattern to fill or stroke with.
+
+ For now, just do colors, but could later do
+ patterns or images too.
+
+ Attributes:
+ color: (r,g,b) triple of floats, 0.0=no color, 1.0=max color
+ """
+
+ def __init__(self, r=0.0, g=0.0, b=0.0):
+ self.color = (r, g, b)
+
+ @staticmethod
+ def CMYK(c, m, y, k):
+ """Return Paint specified in CMYK model.
+
+ Uses formula from 6.2.4 of PDF Reference.
+
+ Args:
+ c, m, y, k: float - in range [0, 1]
+ Returns:
+ Paint - with components in rgb form now
+ """
+
+ return Paint(1.0 - min(1.0, c + k),
+ 1.0 - min(1.0, m + k), 1.0 - min(1.0, y + k))
+
+black_paint = Paint()
+white_paint = Paint(1.0, 1.0, 1.0)
+
+ColorDict = {
+ 'aqua': Paint(0.0, 1.0, 1.0),
+ 'black': Paint(0.0, 0.0, 0.0),
+ 'blue': Paint(0.0, 0.0, 1.0),
+ 'fuchsia': Paint(1.0, 0.0, 1.0),
+ 'gray': Paint(0.5, 0.5, 0.5),
+ 'green': Paint(0.0, 0.5, 0.0),
+ 'lime': Paint(0.0, 1.0, 0.0),
+ 'maroon': Paint(0.5, 0.0, 0.0),
+ 'navy': Paint(0.0, 0.0, 0.5),
+ 'olive': Paint(0.5, 0.5, 0.0),
+ 'purple': Paint(0.5, 0.0, 0.5),
+ 'red': Paint(1.0, 0.0, 0.0),
+ 'silver': Paint(0.75, 0.75, 0.75),
+ 'teal': Paint(0.0, 0.5, 0.5),
+ 'white': Paint(1.0, 1.0, 1.0),
+ 'yellow': Paint(1.0, 1.0, 0.0)
+}
+
+
+class Path(object):
+ """Represents a path in the PDF sense, with painting instructions.
+
+ Attributes:
+ subpaths: list of Subpath objects
+ filled: True if path is to be filled
+ fillevenodd: True if use even-odd rule to fill (else non-zero winding)
+ stroked: True if path is to be stroked
+ fillpaint: Paint to fill with
+ strokepaint: Paint to stroke with
+ """
+
+ def __init__(self):
+ self.subpaths = []
+ self.filled = False
+ self.fillevenodd = False
+ self.stroked = False
+ self.fillpaint = black_paint
+ self.strokepaint = black_paint
+
+ def AddSubpath(self, subpath):
+ """"Add a subpath."""
+
+ self.subpaths.append(subpath)
+
+ def Empty(self):
+ """Returns True if this Path as no subpaths."""
+
+ return not self.subpaths
+
+
+class Subpath(object):
+ """Represents a subpath in PDF sense, either open or closed.
+
+ We'll represent lines, bezier pieces, circular arc pieces
+ as tuples with letters giving segment type in first position
+ and coordinates (2-tuples of floats) in the other positions.
+
+ Segment types:
+ ('L', a, b) - line from a to b
+ ('B', a, b, c, d) - cubic bezier from a to b, with control points c,d
+ ('Q', a, b, c) - quadratic bezier from a to b, with 1 control point c
+ ('A', a, b, rad, xrot, large-arc, ccw) - elliptical arc from a to b,
+ with rad=(rx, ry) as radii, xrot is x-axis rotation in degrees,
+ large-arc is True if arc should be >= 180 degrees,
+ ccw is True if start->end follows counter-clockwise direction
+ (see SVG spec); note that after rad,
+ the rest are floats or bools, not coordinate pairs
+ Note that s[1] and s[2] are the start and end points for any segment s.
+
+ Attributes:
+ segments: list of segment tuples (see above)
+ closed: True if closed
+ """
+
+ def __init__(self):
+ self.segments = []
+ self.closed = False
+
+ def Empty(self):
+ """Returns True if this subpath as no segments."""
+
+ return not self.segments
+
+ def AddSegment(self, seg):
+ """Add a segment."""
+
+ self.segments.append(seg)
+
+ @staticmethod
+ def SegStart(s):
+ """Return start point for segment.
+
+ Args:
+ s: a segment tuple
+ Returns:
+ (float, float): the coordinates of the segment's start point
+ """
+
+ return s[1]
+
+ @staticmethod
+ def SegEnd(s):
+ """Return end point for segment.
+
+ Args:
+ s: a segment tuple
+ Returns:
+ (float, float): the coordinates of the segment's end point
+ """
+
+ return s[2]
+
+
+class TransformMatrix(object):
+ """Transformation matrix for 2d coordinates.
+
+ The transform matrix is:
+ [ a b 0 ]
+ [ c d 0 ]
+ [ e f 1 ]
+ and coordinate tranformation is defined by:
+ [x' y' 1] = [x y 1] x TransformMatrix
+
+ Attributes:
+ a, b, c, d, e, f: floats
+ """
+
+ def __init__(self, a=1.0, b=0.0, c=0.0, d=1.0, e=0.0, f=0.0):
+ self.a = a
+ self.b = b
+ self.c = c
+ self.d = d
+ self.e = e
+ self.f = f
+
+ def __str__(self):
+ return str([self.a, self.b, self.c, self.d, self.e, self.f])
+
+ def Copy(self):
+ """Return a copy of this matrix."""
+
+ return TransformMatrix(self.a, self.b, self.c, self.d, self.e, self.f)
+
+ def ComposeTransform(self, a, b, c, d, e, f):
+ """Apply the transform given the the arguments on top of this one.
+
+ This is accomplished by returning t x sel
+ where t is the transform matrix that would be formed from the args.
+
+ Arguments:
+ a, b, c, d, e, f: float - defines a composing TransformMatrix
+ """
+
+ newa = a * self.a + b * self.c
+ newb = a * self.b + b * self.d
+ newc = c * self.a + d * self.c
+ newd = c * self.b + d * self.d
+ newe = e * self.a + f * self.c + self.e
+ newf = e * self.b + f * self.d + self.f
+ self.a = newa
+ self.b = newb
+ self.c = newc
+ self.d = newd
+ self.e = newe
+ self.f = newf
+
+ def Apply(self, pt):
+ """Return the result of applying this tranform to pt = (x,y).
+
+ Arguments:
+ (x, y) : (float, float)
+ Returns:
+ (x', y'): 2-tuple of floats, the result of [x y 1] x self
+ """
+
+ (x, y) = pt
+ return (self.a * x + self.c * y + self.e, \
+ self.b * x + self.d * y + self.f)
+
+
+def ApproxEqualPoints(p, q):
+ """Return True if p and q are approximately the same points.
+
+ Args:
+ p: n-tuple of float
+ q: n-tuple of float
+ Returns:
+ bool - True if the 1-norm <= DISTTOL
+ """
+
+ for i in range(len(p)):
+ if abs(p[i] - q[i]) > DISTTOL:
+ return False
+ return True
+
+
+def PointInside(v, a, points):
+ """Return 1, 0, or -1 as v is inside, on, or outside polygon.
+
+ Cf. Eric Haines ptinpoly in Graphics Gems IV.
+
+ Args:
+ v : (float, float) or (float, float, float) - coordinates of a point
+ a : list of vertex indices defining polygon (assumed CCW)
+ points: Points - to get coordinates for polygon
+ Returns:
+ 1, 0, -1: as v is inside, on, or outside polygon a
+ """
+
+ (xv, yv) = (v[0], v[1])
+ vlast = points.pos[a[-1]]
+ (x0, y0) = (vlast[0], vlast[1])
+ if x0 == xv and y0 == yv:
+ return 0
+ yflag0 = y0 > yv
+ inside = False
+ n = len(a)
+ for i in range(0, n):
+ vi = points.pos[a[i]]
+ (x1, y1) = (vi[0], vi[1])
+ if x1 == xv and y1 == yv:
+ return 0
+ yflag1 = y1 > yv
+ if yflag0 != yflag1:
+ xflag0 = x0 > xv
+ xflag1 = x1 > xv
+ if xflag0 == xflag1:
+ if xflag0:
+ inside = not inside
+ else:
+ z = x1 - (y1 - yv) * (x0 - x1) / (y0 - y1)
+ if z >= xv:
+ inside = not inside
+ x0 = x1
+ y0 = y1
+ yflag0 = yflag1
+ if inside:
+ return 1
+ else:
+ return -1
+
+
+def SignedArea(polygon, points):
+ """Return the area of the polgon, positive if CCW, negative if CW.
+
+ Args:
+ polygon: list of vertex indices
+ points: Points
+ Returns:
+ float - area of polygon, positive if it was CCW, else negative
+ """
+
+ a = 0.0
+ n = len(polygon)
+ for i in range(0, n):
+ u = points.pos[polygon[i]]
+ v = points.pos[polygon[(i + 1) % n]]
+ a += u[0] * v[1] - u[1] * v[0]
+ return 0.5 * a
+
+
+def VecAdd(a, b):
+ """Return vector a-b.
+
+ Args:
+ a: n-tuple of floats
+ b: n-tuple of floats
+ Returns:
+ n-tuple of floats - pairwise addition a+b
+ """
+
+ n = len(a)
+ assert(n == len(b))
+ return tuple([a[i] + b[i] for i in range(n)])
+
+
+def VecSub(a, b):
+ """Return vector a-b.
+
+ Args:
+ a: n-tuple of floats
+ b: n-tuple of floats
+ Returns:
+ n-tuple of floats - pairwise subtraction a-b
+ """
+
+ n = len(a)
+ assert(n == len(b))
+ return tuple([a[i] - b[i] for i in range(n)])
+
+
+def VecDot(a, b):
+ """Return the dot product of two vectors.
+
+ Args:
+ a: n-tuple of floats
+ b: n-tuple of floats
+ Returns:
+ n-tuple of floats - dot product of a and b
+ """
+
+ n = len(a)
+ assert(n == len(b))
+ sum = 0.0
+ for i in range(n):
+ sum += a[i] * b[i]
+ return sum
+
+
+def VecLen(a):
+ """Return the Euclidean lenght of the argument vector.
+
+ Args:
+ a: n-tuple of floats
+ Returns:
+ float: the 2-norm of a
+ """
+
+ s = 0.0
+ for v in a:
+ s += v * v
+ return math.sqrt(s)
+
+
+def Newell(poly, points):
+ """Use Newell method to find polygon normal.
+
+ Assume poly has length at least 3 and points are 3d.
+
+ Args:
+ poly: list of int - indices into points.pos
+ points: Points - assumed 3d
+ Returns:
+ (float, float, float) - the average normal
+ """
+
+ sumx = 0.0
+ sumy = 0.0
+ sumz = 0.0
+ n = len(poly)
+ pos = points.pos
+ for i, ai in enumerate(poly):
+ bi = poly[(i + 1) % n]
+ a = pos[ai]
+ b = pos[bi]
+ sumx += (a[1] - b[1]) * (a[2] + b[2])
+ sumy += (a[2] - b[2]) * (a[0] + b[0])
+ sumz += (a[0] - b[0]) * (a[1] + b[1])
+ return Norm3(sumx, sumy, sumz)
+
+
+def Norm3(x, y, z):
+ """Return vector (x,y,z) normalized by dividing by squared length.
+ Return (0.0, 0.0, 1.0) if the result is undefined."""
+ sqrlen = x * x + y * y + z * z
+ if sqrlen < 1e-100:
+ return (0.0, 0.0, 1.0)
+ else:
+ try:
+ d = math.sqrt(sqrlen)
+ return (x / d, y / d, z / d)
+ except:
+ return (0.0, 0.0, 1.0)
+
+
+# We're using right-hand coord system, where
+# forefinger=x, middle=y, thumb=z on right hand.
+# Then, e.g., (1,0,0) x (0,1,0) = (0,0,1)
+def Cross3(a, b):
+ """Return the cross product of two vectors, a x b."""
+
+ (ax, ay, az) = a
+ (bx, by, bz) = b
+ return (ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx)
+
+
+def MulPoint3(p, m):
+ """Return matrix multiplication of p times m
+ where m is a 4x3 matrix and p is a 3d point, extended with 1."""
+
+ (x, y, z) = p
+ return (x * m[0] + y * m[3] + z * m[6] + m[9],
+ x * m[1] + y * m[4] + z * m[7] + m[10],
+ x * m[2] + y * m[5] + z * m[8] + m[11])