diff options
Diffstat (limited to 'add_advanced_objects_panels')
-rw-r--r-- | add_advanced_objects_panels/DelaunayVoronoi.py | 998 | ||||
-rw-r--r-- | add_advanced_objects_panels/__init__.py | 467 | ||||
-rw-r--r-- | add_advanced_objects_panels/delaunay_voronoi.py | 312 | ||||
-rw-r--r-- | add_advanced_objects_panels/drop_to_ground.py | 347 | ||||
-rw-r--r-- | add_advanced_objects_panels/object_laplace_lightning.py | 1440 | ||||
-rw-r--r-- | add_advanced_objects_panels/object_mangle_tools.py | 208 | ||||
-rw-r--r-- | add_advanced_objects_panels/oscurart_constellation.py | 141 | ||||
-rw-r--r-- | add_advanced_objects_panels/unfold_transition.py | 346 |
8 files changed, 4259 insertions, 0 deletions
diff --git a/add_advanced_objects_panels/DelaunayVoronoi.py b/add_advanced_objects_panels/DelaunayVoronoi.py new file mode 100644 index 00000000..dcce7f68 --- /dev/null +++ b/add_advanced_objects_panels/DelaunayVoronoi.py @@ -0,0 +1,998 @@ +# -*- coding: utf-8 -*- + +# Voronoi diagram calculator/ Delaunay triangulator +# +# - Voronoi Diagram Sweepline algorithm and C code by Steven Fortune, +# 1987, http://ect.bell-labs.com/who/sjf/ +# - Python translation to file voronoi.py by Bill Simons, 2005, http://www.oxfish.com/ +# - Additional changes for QGIS by Carson Farmer added November 2010 +# - 2012 Ported to Python 3 and additional clip functions by domlysz at gmail.com +# +# Calculate Delaunay triangulation or the Voronoi polygons for a set of +# 2D input points. +# +# Derived from code bearing the following notice: +# +# The author of this software is Steven Fortune. Copyright (c) 1994 by AT&T +# Bell Laboratories. +# Permission to use, copy, modify, and distribute this software for any +# purpose without fee is hereby granted, provided that this entire notice +# is included in all copies of any software which is or includes a copy +# or modification of this software and in all copies of the supporting +# documentation for such software. +# THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED +# WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY +# REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY +# OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. +# +# Comments were incorporated from Shane O'Sullivan's translation of the +# original code into C++ (http://mapviewer.skynet.ie/voronoi.html) +# +# Steve Fortune's homepage: http://netlib.bell-labs.com/cm/cs/who/sjf/index.html +# +# For programmatic use, two functions are available: +# +# computeVoronoiDiagram(points, xBuff, yBuff, polygonsOutput=False, formatOutput=False): +# Takes : +# - a list of point objects (which must have x and y fields). +# - x and y buffer values which are the expansion percentages of the +# bounding box rectangle including all input points. +# Returns : +# - With default options : +# A list of 2-tuples, representing the two points of each Voronoi diagram edge. +# Each point contains 2-tuples which are the x,y coordinates of point. +# if formatOutput is True, returns : +# - a list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices. +# - and a list of 2-tuples (v1, v2) representing edges of the Voronoi diagram. +# v1 and v2 are the indices of the vertices at the end of the edge. +# - If polygonsOutput option is True, returns : +# A dictionary of polygons, keys are the indices of the input points, +# values contains n-tuples representing the n points of each Voronoi diagram polygon. +# Each point contains 2-tuples which are the x,y coordinates of point. +# if formatOutput is True, returns : +# - A list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices. +# - and a dictionary of input points indices. Values contains n-tuples representing +# the n points of each Voronoi diagram polygon. +# Each tuple contains the vertex indices of the polygon vertices. +# +# computeDelaunayTriangulation(points): +# Takes a list of point objects (which must have x and y fields). +# Returns a list of 3-tuples: the indices of the points that form a Delaunay triangle. + +import bpy +import math + +# Globals +TOLERANCE = 1e-9 +BIG_FLOAT = 1e38 + + +class Context(object): + + def __init__(self): + self.doPrint = 0 + self.debug = 0 + + # tuple (xmin, xmax, ymin, ymax) + self.extent = () + self.triangulate = False + # list of vertex 2-tuples: (x,y) + self.vertices = [] + # equation of line 3-tuple (a b c), for the equation of the line a*x+b*y = c + self.lines = [] + + # edge 3-tuple: (line index, vertex 1 index, vertex 2 index) + # if either vertex index is -1, the edge extends to infinity + self.edges = [] + # 3-tuple of vertex indices + self.triangles = [] + # a dict of site:[edges] pairs + self.polygons = {} + + +# Clip functions # + def getClipEdges(self): + xmin, xmax, ymin, ymax = self.extent + clipEdges = [] + for edge in self.edges: + equation = self.lines[edge[0]] # line equation + if edge[1] != -1 and edge[2] != -1: # finite line + x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] + x2, y2 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] + pt1, pt2 = (x1, y1), (x2, y2) + inExtentP1, inExtentP2 = self.inExtent(x1, y1), self.inExtent(x2, y2) + if inExtentP1 and inExtentP2: + clipEdges.append((pt1, pt2)) + elif inExtentP1 and not inExtentP2: + pt2 = self.clipLine(x1, y1, equation, leftDir=False) + clipEdges.append((pt1, pt2)) + elif not inExtentP1 and inExtentP2: + pt1 = self.clipLine(x2, y2, equation, leftDir=True) + clipEdges.append((pt1, pt2)) + else: # infinite line + if edge[1] != -1: + x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] + leftDir = False + else: + x1, y1 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] + leftDir = True + if self.inExtent(x1, y1): + pt1 = (x1, y1) + pt2 = self.clipLine(x1, y1, equation, leftDir) + clipEdges.append((pt1, pt2)) + return clipEdges + + def getClipPolygons(self, closePoly): + xmin, xmax, ymin, ymax = self.extent + poly = {} + for inPtsIdx, edges in self.polygons.items(): + clipEdges = [] + for edge in edges: + equation = self.lines[edge[0]] # line equation + if edge[1] != -1 and edge[2] != -1: # finite line + x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] + x2, y2 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] + pt1, pt2 = (x1, y1), (x2, y2) + inExtentP1, inExtentP2 = self.inExtent(x1, y1), self.inExtent(x2, y2) + if inExtentP1 and inExtentP2: + clipEdges.append((pt1, pt2)) + elif inExtentP1 and not inExtentP2: + pt2 = self.clipLine(x1, y1, equation, leftDir=False) + clipEdges.append((pt1, pt2)) + elif not inExtentP1 and inExtentP2: + pt1 = self.clipLine(x2, y2, equation, leftDir=True) + clipEdges.append((pt1, pt2)) + else: # infinite line + if edge[1] != -1: + x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] + leftDir = False + else: + x1, y1 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] + leftDir = True + if self.inExtent(x1, y1): + pt1 = (x1, y1) + pt2 = self.clipLine(x1, y1, equation, leftDir) + clipEdges.append((pt1, pt2)) + # create polygon definition from edges and check if polygon is completely closed + polyPts, complete = self.orderPts(clipEdges) + if not complete: + startPt = polyPts[0] + endPt = polyPts[-1] + # if start & end points are collinear then they are along an extent border + if startPt[0] == endPt[0] or startPt[1] == endPt[1]: + polyPts.append(polyPts[0]) # simple close + else: # close at extent corner + # upper left + if (startPt[0] == xmin and endPt[1] == ymax) or (endPt[0] == xmin and startPt[1] == ymax): + polyPts.append((xmin, ymax)) # corner point + polyPts.append(polyPts[0]) # close polygon + # upper right + if (startPt[0] == xmax and endPt[1] == ymax) or (endPt[0] == xmax and startPt[1] == ymax): + polyPts.append((xmax, ymax)) + polyPts.append(polyPts[0]) + # bottom right + if (startPt[0] == xmax and endPt[1] == ymin) or (endPt[0] == xmax and startPt[1] == ymin): + polyPts.append((xmax, ymin)) + polyPts.append(polyPts[0]) + # bottom left + if (startPt[0] == xmin and endPt[1] == ymin) or (endPt[0] == xmin and startPt[1] == ymin): + polyPts.append((xmin, ymin)) + polyPts.append(polyPts[0]) + if not closePoly: # unclose polygon + polyPts = polyPts[:-1] + poly[inPtsIdx] = polyPts + return poly + + def clipLine(self, x1, y1, equation, leftDir): + xmin, xmax, ymin, ymax = self.extent + a, b, c = equation + if b == 0: # vertical line + if leftDir: # left is bottom of vertical line + return (x1, ymax) + else: + return (x1, ymin) + elif a == 0: # horizontal line + if leftDir: + return (xmin, y1) + else: + return (xmax, y1) + else: + y2_at_xmin = (c - a * xmin) / b + y2_at_xmax = (c - a * xmax) / b + x2_at_ymin = (c - b * ymin) / a + x2_at_ymax = (c - b * ymax) / a + intersectPts = [] + if ymin <= y2_at_xmin <= ymax: # valid intersect point + intersectPts.append((xmin, y2_at_xmin)) + if ymin <= y2_at_xmax <= ymax: + intersectPts.append((xmax, y2_at_xmax)) + if xmin <= x2_at_ymin <= xmax: + intersectPts.append((x2_at_ymin, ymin)) + if xmin <= x2_at_ymax <= xmax: + intersectPts.append((x2_at_ymax, ymax)) + # delete duplicate (happens if intersect point is at extent corner) + intersectPts = set(intersectPts) + # choose target intersect point + if leftDir: + pt = min(intersectPts) # smaller x value + else: + pt = max(intersectPts) + return pt + + def inExtent(self, x, y): + xmin, xmax, ymin, ymax = self.extent + return x >= xmin and x <= xmax and y >= ymin and y <= ymax + + def orderPts(self, edges): + poly = [] # returned polygon points list [pt1, pt2, pt3, pt4 ....] + pts = [] + # get points list + for edge in edges: + pts.extend([pt for pt in edge]) + # try to get start & end point + try: + startPt, endPt = [pt for pt in pts if pts.count(pt) < 2] # start and end point aren't duplicate + except: # all points are duplicate --> polygon is complete --> append some or other edge points + complete = True + firstIdx = 0 + poly.append(edges[0][0]) + poly.append(edges[0][1]) + else: # incomplete --> append the first edge points + complete = False + # search first edge + for i, edge in enumerate(edges): + if startPt in edge: # find + firstIdx = i + break + poly.append(edges[firstIdx][0]) + poly.append(edges[firstIdx][1]) + if poly[0] != startPt: + poly.reverse() + # append next points in list + del edges[firstIdx] + while edges: # all points will be treated when edges list will be empty + currentPt = poly[-1] # last item + for i, edge in enumerate(edges): + if currentPt == edge[0]: + poly.append(edge[1]) + break + elif currentPt == edge[1]: + poly.append(edge[0]) + break + del edges[i] + return poly, complete + + def setClipBuffer(self, xpourcent, ypourcent): + xmin, xmax, ymin, ymax = self.extent + witdh = xmax - xmin + height = ymax - ymin + xmin = xmin - witdh * xpourcent / 100 + xmax = xmax + witdh * xpourcent / 100 + ymin = ymin - height * ypourcent / 100 + ymax = ymax + height * ypourcent / 100 + self.extent = xmin, xmax, ymin, ymax + + # End clip functions # + + def outSite(self, s): + if(self.debug): + print("site (%d) at %f %f" % (s.sitenum, s.x, s.y)) + elif(self.triangulate): + pass + elif(self.doPrint): + print("s %f %f" % (s.x, s.y)) + + def outVertex(self, s): + self.vertices.append((s.x, s.y)) + if(self.debug): + print("vertex(%d) at %f %f" % (s.sitenum, s.x, s.y)) + elif(self.triangulate): + pass + elif(self.doPrint): + print("v %f %f" % (s.x, s.y)) + + def outTriple(self, s1, s2, s3): + self.triangles.append((s1.sitenum, s2.sitenum, s3.sitenum)) + if (self.debug): + print("circle through left=%d right=%d bottom=%d" % (s1.sitenum, s2.sitenum, s3.sitenum)) + elif (self.triangulate and self.doPrint): + print("%d %d %d" % (s1.sitenum, s2.sitenum, s3.sitenum)) + + def outBisector(self, edge): + self.lines.append((edge.a, edge.b, edge.c)) + if (self.debug): + print("line(%d) %gx+%gy=%g, bisecting %d %d" % (edge.edgenum, edge.a, edge.b, + edge.c, edge.reg[0].sitenum, + edge.reg[1].sitenum) + ) + elif(self.doPrint): + print("l %f %f %f" % (edge.a, edge.b, edge.c)) + + def outEdge(self, edge): + sitenumL = -1 + if edge.ep[Edge.LE] is not None: + sitenumL = edge.ep[Edge.LE].sitenum + sitenumR = -1 + if edge.ep[Edge.RE] is not None: + sitenumR = edge.ep[Edge.RE].sitenum + + # polygons dict add by CF + if edge.reg[0].sitenum not in self.polygons: + self.polygons[edge.reg[0].sitenum] = [] + if edge.reg[1].sitenum not in self.polygons: + self.polygons[edge.reg[1].sitenum] = [] + self.polygons[edge.reg[0].sitenum].append((edge.edgenum, sitenumL, sitenumR)) + self.polygons[edge.reg[1].sitenum].append((edge.edgenum, sitenumL, sitenumR)) + + self.edges.append((edge.edgenum, sitenumL, sitenumR)) + + if (not self.triangulate): + if (self.doPrint): + print("e %d" % edge.edgenum) + print(" %d " % sitenumL) + print("%d" % sitenumR) + + +def voronoi(siteList, context): + context.extent = siteList.extent + edgeList = EdgeList(siteList.xmin, siteList.xmax, len(siteList)) + priorityQ = PriorityQueue(siteList.ymin, siteList.ymax, len(siteList)) + siteIter = siteList.iterator() + + bottomsite = siteIter.next() + context.outSite(bottomsite) + newsite = siteIter.next() + minpt = Site(-BIG_FLOAT, -BIG_FLOAT) + while True: + if not priorityQ.isEmpty(): + minpt = priorityQ.getMinPt() + + if (newsite and (priorityQ.isEmpty() or newsite < minpt)): + # newsite is smallest - this is a site event + context.outSite(newsite) + + # get first Halfedge to the LEFT and RIGHT of the new site + lbnd = edgeList.leftbnd(newsite) + rbnd = lbnd.right + + # if this halfedge has no edge, bot = bottom site (whatever that is) + # create a new edge that bisects + bot = lbnd.rightreg(bottomsite) + edge = Edge.bisect(bot, newsite) + context.outBisector(edge) + + # create a new Halfedge, setting its pm field to 0 and insert + # this new bisector edge between the left and right vectors in + # a linked list + bisector = Halfedge(edge, Edge.LE) + edgeList.insert(lbnd, bisector) + + # if the new bisector intersects with the left edge, remove + # the left edge's vertex, and put in the new one + p = lbnd.intersect(bisector) + if p is not None: + priorityQ.delete(lbnd) + priorityQ.insert(lbnd, p, newsite.distance(p)) + + # create a new Halfedge, setting its pm field to 1 + # insert the new Halfedge to the right of the original bisector + lbnd = bisector + bisector = Halfedge(edge, Edge.RE) + edgeList.insert(lbnd, bisector) + + # if this new bisector intersects with the right Halfedge + p = bisector.intersect(rbnd) + if p is not None: + # push the Halfedge into the ordered linked list of vertices + priorityQ.insert(bisector, p, newsite.distance(p)) + + newsite = siteIter.next() + + elif not priorityQ.isEmpty(): + # intersection is smallest - this is a vector (circle) event + # pop the Halfedge with the lowest vector off the ordered list of + # vectors. Get the Halfedge to the left and right of the above HE + # and also the Halfedge to the right of the right HE + lbnd = priorityQ.popMinHalfedge() + llbnd = lbnd.left + rbnd = lbnd.right + rrbnd = rbnd.right + + # get the Site to the left of the left HE and to the right of + # the right HE which it bisects + bot = lbnd.leftreg(bottomsite) + top = rbnd.rightreg(bottomsite) + + # output the triple of sites, stating that a circle goes through them + mid = lbnd.rightreg(bottomsite) + context.outTriple(bot, top, mid) + + # get the vertex that caused this event and set the vertex number + # couldn't do this earlier since we didn't know when it would be processed + v = lbnd.vertex + siteList.setSiteNumber(v) + context.outVertex(v) + + # set the endpoint of the left and right Halfedge to be this vector + if lbnd.edge.setEndpoint(lbnd.pm, v): + context.outEdge(lbnd.edge) + + if rbnd.edge.setEndpoint(rbnd.pm, v): + context.outEdge(rbnd.edge) + + # delete the lowest HE, remove all vertex events to do with the + # right HE and delete the right HE + edgeList.delete(lbnd) + priorityQ.delete(rbnd) + edgeList.delete(rbnd) + + # if the site to the left of the event is higher than the Site + # to the right of it, then swap them and set 'pm' to RIGHT + pm = Edge.LE + if bot.y > top.y: + bot, top = top, bot + pm = Edge.RE + + # Create an Edge (or line) that is between the two Sites. This + # creates the formula of the line, and assigns a line number to it + edge = Edge.bisect(bot, top) + context.outBisector(edge) + + # create a HE from the edge + bisector = Halfedge(edge, pm) + + # insert the new bisector to the right of the left HE + # set one endpoint to the new edge to be the vector point 'v' + # If the site to the left of this bisector is higher than the right + # Site, then this endpoint is put in position 0; otherwise in pos 1 + edgeList.insert(llbnd, bisector) + if edge.setEndpoint(Edge.RE - pm, v): + context.outEdge(edge) + + # if left HE and the new bisector don't intersect, then delete + # the left HE, and reinsert it + p = llbnd.intersect(bisector) + if p is not None: + priorityQ.delete(llbnd) + priorityQ.insert(llbnd, p, bot.distance(p)) + + # if right HE and the new bisector don't intersect, then reinsert it + p = bisector.intersect(rrbnd) + if p is not None: + priorityQ.insert(bisector, p, bot.distance(p)) + else: + break + + he = edgeList.leftend.right + while he is not edgeList.rightend: + context.outEdge(he.edge) + he = he.right + Edge.EDGE_NUM = 0 # CF + + +def isEqual(a, b, relativeError=TOLERANCE): + # is nearly equal to within the allowed relative error + norm = max(abs(a), abs(b)) + return (norm < relativeError) or (abs(a - b) < (relativeError * norm)) + + +class Site(object): + + def __init__(self, x=0.0, y=0.0, sitenum=0): + self.x = x + self.y = y + self.sitenum = sitenum + + def dump(self): + print("Site #%d (%g, %g)" % (self.sitenum, self.x, self.y)) + + def __lt__(self, other): + if self.y < other.y: + return True + elif self.y > other.y: + return False + elif self.x < other.x: + return True + elif self.x > other.x: + return False + else: + return False + + def __eq__(self, other): + if self.y == other.y and self.x == other.x: + return True + + def distance(self, other): + dx = self.x - other.x + dy = self.y - other.y + return math.sqrt(dx * dx + dy * dy) + + +class Edge(object): + LE = 0 # left end indice --> edge.ep[Edge.LE] + RE = 1 # right end indice + EDGE_NUM = 0 + DELETED = {} # marker value + + def __init__(self): + self.a = 0.0 # equation of the line a*x+b*y = c + self.b = 0.0 + self.c = 0.0 + self.ep = [None, None] # end point (2 tuples of site) + self.reg = [None, None] + self.edgenum = 0 + + def dump(self): + print("(#%d a=%g, b=%g, c=%g)" % (self.edgenum, self.a, self.b, self.c)) + print("ep", self.ep) + print("reg", self.reg) + + def setEndpoint(self, lrFlag, site): + self.ep[lrFlag] = site + if self.ep[Edge.RE - lrFlag] is None: + return False + return True + + @staticmethod + def bisect(s1, s2): + newedge = Edge() + newedge.reg[0] = s1 # store the sites that this edge is bisecting + newedge.reg[1] = s2 + + # to begin with, there are no endpoints on the bisector - it goes to infinity + # ep[0] and ep[1] are None + + # get the difference in x dist between the sites + dx = float(s2.x - s1.x) + dy = float(s2.y - s1.y) + adx = abs(dx) # make sure that the difference in positive + ady = abs(dy) + + # get the slope of the line + newedge.c = float(s1.x * dx + s1.y * dy + (dx * dx + dy * dy) * 0.5) + if adx > ady: + # set formula of line, with x fixed to 1 + newedge.a = 1.0 + newedge.b = dy / dx + newedge.c /= dx + else: + # set formula of line, with y fixed to 1 + newedge.b = 1.0 + newedge.a = dx / dy + newedge.c /= dy + + newedge.edgenum = Edge.EDGE_NUM + Edge.EDGE_NUM += 1 + return newedge + + +class Halfedge(object): + + def __init__(self, edge=None, pm=Edge.LE): + self.left = None # left Halfedge in the edge list + self.right = None # right Halfedge in the edge list + self.qnext = None # priority queue linked list pointer + self.edge = edge # edge list Edge + self.pm = pm + self.vertex = None # Site() + self.ystar = BIG_FLOAT + + def dump(self): + print("Halfedge--------------------------") + print("left: ", self.left) + print("right: ", self.right) + print("edge: ", self.edge) + print("pm: ", self.pm) + print("vertex: "), + if self.vertex: + self.vertex.dump() + else: + print("None") + print("ystar: ", self.ystar) + + def __lt__(self, other): + if self.ystar < other.ystar: + return True + elif self.ystar > other.ystar: + return False + elif self.vertex.x < other.vertex.x: + return True + elif self.vertex.x > other.vertex.x: + return False + else: + return False + + def __eq__(self, other): + if self.ystar == other.ystar and self.vertex.x == other.vertex.x: + return True + + def leftreg(self, default): + if not self.edge: + return default + elif self.pm == Edge.LE: + return self.edge.reg[Edge.LE] + else: + return self.edge.reg[Edge.RE] + + def rightreg(self, default): + if not self.edge: + return default + elif self.pm == Edge.LE: + return self.edge.reg[Edge.RE] + else: + return self.edge.reg[Edge.LE] + + # returns True if p is to right of halfedge self + def isPointRightOf(self, pt): + e = self.edge + topsite = e.reg[1] + right_of_site = pt.x > topsite.x + + if(right_of_site and self.pm == Edge.LE): + return True + + if(not right_of_site and self.pm == Edge.RE): + return False + + if(e.a == 1.0): + dyp = pt.y - topsite.y + dxp = pt.x - topsite.x + fast = 0 + if ((not right_of_site and e.b < 0.0) or (right_of_site and e.b >= 0.0)): + above = dyp >= e.b * dxp + fast = above + else: + above = pt.x + pt.y * e.b > e.c + if(e.b < 0.0): + above = not above + if (not above): + fast = 1 + if (not fast): + dxs = topsite.x - (e.reg[0]).x + above = e.b * (dxp * dxp - dyp * dyp) < dxs * dyp * (1.0 + 2.0 * dxp / dxs + e.b * e.b) + if(e.b < 0.0): + above = not above + else: # e.b == 1.0 + yl = e.c - e.a * pt.x + t1 = pt.y - yl + t2 = pt.x - topsite.x + t3 = yl - topsite.y + above = t1 * t1 > t2 * t2 + t3 * t3 + + if(self.pm == Edge.LE): + return above + else: + return not above + + # create a new site where the Halfedges el1 and el2 intersect + def intersect(self, other): + e1 = self.edge + e2 = other.edge + if (e1 is None) or (e2 is None): + return None + + # if the two edges bisect the same parent return None + if e1.reg[1] is e2.reg[1]: + return None + + d = e1.a * e2.b - e1.b * e2.a + if isEqual(d, 0.0): + return None + + xint = (e1.c * e2.b - e2.c * e1.b) / d + yint = (e2.c * e1.a - e1.c * e2.a) / d + if e1.reg[1] < e2.reg[1]: + he = self + e = e1 + else: + he = other + e = e2 + + rightOfSite = xint >= e.reg[1].x + if((rightOfSite and he.pm == Edge.LE) or + (not rightOfSite and he.pm == Edge.RE)): + return None + + # create a new site at the point of intersection - this is a new + # vector event waiting to happen + return Site(xint, yint) + + +class EdgeList(object): + + def __init__(self, xmin, xmax, nsites): + if xmin > xmax: + xmin, xmax = xmax, xmin + self.hashsize = int(2 * math.sqrt(nsites + 4)) + + self.xmin = xmin + self.deltax = float(xmax - xmin) + self.hash = [None] * self.hashsize + + self.leftend = Halfedge() + self.rightend = Halfedge() + self.leftend.right = self.rightend + self.rightend.left = self.leftend + self.hash[0] = self.leftend + self.hash[-1] = self.rightend + + def insert(self, left, he): + he.left = left + he.right = left.right + left.right.left = he + left.right = he + + def delete(self, he): + he.left.right = he.right + he.right.left = he.left + he.edge = Edge.DELETED + + # Get entry from hash table, pruning any deleted nodes + def gethash(self, b): + if(b < 0 or b >= self.hashsize): + return None + he = self.hash[b] + if he is None or he.edge is not Edge.DELETED: + return he + + # Hash table points to deleted half edge. Patch as necessary. + self.hash[b] = None + return None + + def leftbnd(self, pt): + # Use hash table to get close to desired halfedge + bucket = int(((pt.x - self.xmin) / self.deltax * self.hashsize)) + + if(bucket < 0): + bucket = 0 + + if(bucket >= self.hashsize): + bucket = self.hashsize - 1 + + he = self.gethash(bucket) + if(he is None): + i = 1 + while True: + he = self.gethash(bucket - i) + if (he is not None): + break + he = self.gethash(bucket + i) + if (he is not None): + break + i += 1 + + # Now search linear list of halfedges for the corect one + if (he is self.leftend) or (he is not self.rightend and he.isPointRightOf(pt)): + he = he.right + while he is not self.rightend and he.isPointRightOf(pt): + he = he.right + he = he.left + else: + he = he.left + while (he is not self.leftend and not he.isPointRightOf(pt)): + he = he.left + + # Update hash table and reference counts + if(bucket > 0 and bucket < self.hashsize - 1): + self.hash[bucket] = he + return he + + +class PriorityQueue(object): + + def __init__(self, ymin, ymax, nsites): + self.ymin = ymin + self.deltay = ymax - ymin + self.hashsize = int(4 * math.sqrt(nsites)) + self.count = 0 + self.minidx = 0 + self.hash = [] + for i in range(self.hashsize): + self.hash.append(Halfedge()) + + def __len__(self): + return self.count + + def isEmpty(self): + return self.count == 0 + + def insert(self, he, site, offset): + he.vertex = site + he.ystar = site.y + offset + last = self.hash[self.getBucket(he)] + next = last.qnext + while((next is not None) and he > next): + last = next + next = last.qnext + he.qnext = last.qnext + last.qnext = he + self.count += 1 + + def delete(self, he): + if (he.vertex is not None): + last = self.hash[self.getBucket(he)] + while last.qnext is not he: + last = last.qnext + last.qnext = he.qnext + self.count -= 1 + he.vertex = None + + def getBucket(self, he): + bucket = int(((he.ystar - self.ymin) / self.deltay) * self.hashsize) + if bucket < 0: + bucket = 0 + if bucket >= self.hashsize: + bucket = self.hashsize - 1 + if bucket < self.minidx: + self.minidx = bucket + return bucket + + def getMinPt(self): + while(self.hash[self.minidx].qnext is None): + self.minidx += 1 + he = self.hash[self.minidx].qnext + x = he.vertex.x + y = he.ystar + return Site(x, y) + + def popMinHalfedge(self): + curr = self.hash[self.minidx].qnext + self.hash[self.minidx].qnext = curr.qnext + self.count -= 1 + return curr + + +class SiteList(object): + + def __init__(self, pointList): + self.__sites = [] + self.__sitenum = 0 + + self.__xmin = min([pt.x for pt in pointList]) + self.__ymin = min([pt.y for pt in pointList]) + self.__xmax = max([pt.x for pt in pointList]) + self.__ymax = max([pt.y for pt in pointList]) + self.__extent = (self.__xmin, self.__xmax, self.__ymin, self.__ymax) + + for i, pt in enumerate(pointList): + self.__sites.append(Site(pt.x, pt.y, i)) + self.__sites.sort() + + def setSiteNumber(self, site): + site.sitenum = self.__sitenum + self.__sitenum += 1 + + class Iterator(object): + + def __init__(this, lst): + this.generator = (s for s in lst) + + def __iter__(this): + return this + + def next(this): + try: + # Note: Blender is Python 3.x so no need for 2.x checks + return this.generator.__next__() + except StopIteration: + return None + + def iterator(self): + return SiteList.Iterator(self.__sites) + + def __iter__(self): + return SiteList.Iterator(self.__sites) + + def __len__(self): + return len(self.__sites) + + def _getxmin(self): + return self.__xmin + + def _getymin(self): + return self.__ymin + + def _getxmax(self): + return self.__xmax + + def _getymax(self): + return self.__ymax + + def _getextent(self): + return self.__extent + + xmin = property(_getxmin) + ymin = property(_getymin) + xmax = property(_getxmax) + ymax = property(_getymax) + extent = property(_getextent) + + +def computeVoronoiDiagram(points, xBuff=0, yBuff=0, polygonsOutput=False, + formatOutput=False, closePoly=True): + """ + Takes : + - a list of point objects (which must have x and y fields). + - x and y buffer values which are the expansion percentages of the bounding box + rectangle including all input points. + Returns : + - With default options : + A list of 2-tuples, representing the two points of each Voronoi diagram edge. + Each point contains 2-tuples which are the x,y coordinates of point. + if formatOutput is True, returns : + - a list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices. + - and a list of 2-tuples (v1, v2) representing edges of the Voronoi diagram. + v1 and v2 are the indices of the vertices at the end of the edge. + - If polygonsOutput option is True, returns : + A dictionary of polygons, keys are the indices of the input points, + values contains n-tuples representing the n points of each Voronoi diagram polygon. + Each point contains 2-tuples which are the x,y coordinates of point. + if formatOutput is True, returns : + - A list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices. + - and a dictionary of input points indices. Values contains n-tuples representing + the n points of each Voronoi diagram polygon. + Each tuple contains the vertex indices of the polygon vertices. + - if closePoly is True then, in the list of points of a polygon, last point will be the same of first point + """ + siteList = SiteList(points) + context = Context() + voronoi(siteList, context) + context.setClipBuffer(xBuff, yBuff) + if not polygonsOutput: + clipEdges = context.getClipEdges() + if formatOutput: + vertices, edgesIdx = formatEdgesOutput(clipEdges) + return vertices, edgesIdx + else: + return clipEdges + else: + clipPolygons = context.getClipPolygons(closePoly) + if formatOutput: + vertices, polyIdx = formatPolygonsOutput(clipPolygons) + return vertices, polyIdx + else: + return clipPolygons + + +def formatEdgesOutput(edges): + # get list of points + pts = [] + for edge in edges: + pts.extend(edge) + # get unique values + pts = set(pts) # unique values (tuples are hashable) + # get dict {values:index} + valuesIdxDict = dict(zip(pts, range(len(pts)))) + # get edges index reference + edgesIdx = [] + for edge in edges: + edgesIdx.append([valuesIdxDict[pt] for pt in edge]) + return list(pts), edgesIdx + + +def formatPolygonsOutput(polygons): + # get list of points + pts = [] + for poly in polygons.values(): + pts.extend(poly) + # get unique values + pts = set(pts) # unique values (tuples are hashable) + # get dict {values:index} + valuesIdxDict = dict(zip(pts, range(len(pts)))) + # get polygons index reference + polygonsIdx = {} + for inPtsIdx, poly in polygons.items(): + polygonsIdx[inPtsIdx] = [valuesIdxDict[pt] for pt in poly] + return list(pts), polygonsIdx + + +def computeDelaunayTriangulation(points): + """ Takes a list of point objects (which must have x and y fields). + Returns a list of 3-tuples: the indices of the points that form a + Delaunay triangle. + """ + siteList = SiteList(points) + context = Context() + context.triangulate = True + voronoi(siteList, context) + return context.triangles diff --git a/add_advanced_objects_panels/__init__.py b/add_advanced_objects_panels/__init__.py new file mode 100644 index 00000000..81950727 --- /dev/null +++ b/add_advanced_objects_panels/__init__.py @@ -0,0 +1,467 @@ +# ##### 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 ##### + +# Contributed to by: +# meta-androcto, Bill Currie, Jorge Hernandez - Melenedez Jacob Morris, Oscurart # +# Rebellion, Antonis Karvelas, Eleanor Howick, lijenstina, Daniel Schalla, Domlysz # +# Unnikrishnan(kodemax), Florian Meyer, Omar ahmed, Brian Hinton (Nichod), liero # +# Atom, Dannyboy, Mano-Wii, Kursad Karatas, teldredge, Phil Cote # + +bl_info = { + "name": "Add Advanced Object Panels", + "author": "meta-androcto,", + "version": (1, 1, 4), + "blender": (2, 7, 7), + "description": "Individual Create Panel Activation List", + "location": "Addons Preferences", + "warning": "", + "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/" + "Scripts/3D_interaction/viewport_pies", + "category": "Object" + } + +import bpy +from bpy.types import ( + Menu, + AddonPreferences, + PropertyGroup, + ) +from bpy.props import ( + BoolProperty, + BoolVectorProperty, + EnumProperty, + FloatProperty, + FloatVectorProperty, + IntProperty, + StringProperty, + PointerProperty, + ) + +sub_modules_names = ( + "drop_to_ground", + "object_laplace_lightning", + "object_mangle_tools", + "unfold_transition", + "delaunay_voronoi", + "oscurart_constellation", + ) + + +sub_modules = [__import__(__package__ + "." + submod, {}, {}, submod) for submod in sub_modules_names] +sub_modules.sort(key=lambda mod: (mod.bl_info['category'], mod.bl_info['name'])) + + +#Addons Preferences +def _get_pref_class(mod): + import inspect + + for obj in vars(mod).values(): + if inspect.isclass(obj) and issubclass(obj, PropertyGroup): + if hasattr(obj, 'bl_idname') and obj.bl_idname == mod.__name__: + return obj + + +def get_addon_preferences(name=''): + """Acquisition and registration""" + addons = bpy.context.user_preferences.addons + if __name__ not in addons: # wm.read_factory_settings() + return None + addon_prefs = addons[__name__].preferences + if name: + if not hasattr(addon_prefs, name): + for mod in sub_modules: + if mod.__name__.split('.')[-1] == name: + cls = _get_pref_class(mod) + if cls: + prop = PointerProperty(type=cls) + setattr(AdvancedObjPreferences1, name, prop) + bpy.utils.unregister_class(AdvancedObjPreferences1) + bpy.utils.register_class(AdvancedObjPreferences1) + return getattr(addon_prefs, name, None) + else: + return addon_prefs + + +def register_submodule(mod): + if not hasattr(mod, '__addon_enabled__'): + mod.__addon_enabled__ = False + if not mod.__addon_enabled__: + mod.register() + mod.__addon_enabled__ = True + + +def unregister_submodule(mod): + if mod.__addon_enabled__: + mod.unregister() + mod.__addon_enabled__ = False + + prefs = get_addon_preferences() + name = mod.__name__.split('.')[-1] + if hasattr(AdvancedObjPreferences1, name): + delattr(AdvancedObjPreferences1, name) + if prefs: + bpy.utils.unregister_class(AdvancedObjPreferences1) + bpy.utils.register_class(AdvancedObjPreferences1) + if name in prefs: + del prefs[name] + + +class AdvancedObjPreferences1(AddonPreferences): + bl_idname = __name__ + + def draw(self, context): + layout = self.layout + + for mod in sub_modules: + mod_name = mod.__name__.split('.')[-1] + info = mod.bl_info + column = layout.column() + box = column.box() + + # first stage + expand = getattr(self, 'show_expanded_' + mod_name) + icon = 'TRIA_DOWN' if expand else 'TRIA_RIGHT' + col = box.column() + row = col.row() + sub = row.row() + sub.context_pointer_set('addon_prefs', self) + op = sub.operator('wm.context_toggle', text='', icon=icon, + emboss=False) + op.data_path = 'addon_prefs.show_expanded_' + mod_name + sub.label('{}: {}'.format(info['category'], info['name'])) + sub = row.row() + sub.alignment = 'RIGHT' + if info.get('warning'): + sub.label('', icon='ERROR') + sub.prop(self, 'use_' + mod_name, text='') + + # The second stage + if expand: + if info.get('description'): + split = col.row().split(percentage=0.15) + split.label('Description:') + split.label(info['description']) + if info.get('location'): + split = col.row().split(percentage=0.15) + split.label('Location:') + split.label(info['location']) + if info.get('author') and info.get('author') != 'chromoly': + split = col.row().split(percentage=0.15) + split.label('Author:') + split.label(info['author']) + if info.get('version'): + split = col.row().split(percentage=0.15) + split.label('Version:') + split.label('.'.join(str(x) for x in info['version']), + translate=False) + if info.get('warning'): + split = col.row().split(percentage=0.15) + split.label('Warning:') + split.label(' ' + info['warning'], icon='ERROR') + + tot_row = int(bool(info.get('wiki_url'))) + if tot_row: + split = col.row().split(percentage=0.15) + split.label(text='Internet:') + if info.get('wiki_url'): + op = split.operator('wm.url_open', + text='Documentation', icon='HELP') + op.url = info.get('wiki_url') + for i in range(4 - tot_row): + split.separator() + + # Details and settings + if getattr(self, 'use_' + mod_name): + prefs = get_addon_preferences(mod_name) + + if prefs and hasattr(prefs, 'draw'): + box = box.column() + prefs.layout = box + try: + prefs.draw(context) + except: + traceback.print_exc() + box.label(text='Error (see console)', icon='ERROR') + del prefs.layout + + row = layout.row() + row.label("End of Panel Activations") + + +for mod in sub_modules: + info = mod.bl_info + mod_name = mod.__name__.split('.')[-1] + + def gen_update(mod): + def update(self, context): + if getattr(self, 'use_' + mod.__name__.split('.')[-1]): + if not mod.__addon_enabled__: + register_submodule(mod) + else: + if mod.__addon_enabled__: + unregister_submodule(mod) + return update + + prop = BoolProperty( + name=info['name'], + description=info.get('description', ''), + update=gen_update(mod), + ) + setattr(AdvancedObjPreferences1, 'use_' + mod_name, prop) + prop = BoolProperty() + setattr(AdvancedObjPreferences1, 'show_expanded_' + mod_name, prop) + + +class AdvancedObjProperties1(PropertyGroup): + + # main properties + + # object_laplace_lighting props + ORIGIN = FloatVectorProperty( + name="Origin charge" + ) + GROUNDZ = IntProperty( + name="Ground Z coordinate" + ) + HORDER = IntProperty( + name="Secondary paths orders", + default=1 + ) + # object_laplace_lighting UI props + TSTEPS = IntProperty( + name="Iterations", + default=350, + description="Number of cells to create\n" + "Will end early if hits ground plane or cloud" + ) + GSCALE = FloatProperty( + name="Grid unit size", + default=0.12, + description="scale of cells, .25 = 4 cells per blenderUnit" + ) + BIGVAR = FloatProperty( + name="Straightness", + default=6.3, + description="Straightness/branchiness of bolt, \n" + "<2 is mush, >12 is staight line, 6.3 is good" + ) + GROUNDBOOL = BoolProperty( + name="Use Ground object", + description="Use ground plane or not", + default=True + ) + GROUNDC = IntProperty( + name="Ground charge", + default=-250, + description="Charge of the ground plane" + ) + CLOUDBOOL = BoolProperty( + name="Use Cloud object", + default=False, + description="Use cloud object - attracts and terminates like ground but\n" + "any obj instead of z plane\n" + "Can slow down loop if obj is large, overrides ground" + ) + CLOUDC = IntProperty( + name="Cloud charge", + default=-1, + description="Charge of a cell in cloud object\n" + "(so total charge also depends on obj size)" + ) + VMMESH = BoolProperty( + name="Multi mesh", + default=True, + description="Output to multi-meshes for different materials on main/sec/side branches" + ) + VSMESH = BoolProperty( + name="Single mesh", + default=False, + description="Output to single mesh for using build modifier and particles for effects" + ) + VCUBE = BoolProperty( + name="Cubes", + default=False, + description="CTRL-J after run to JOIN\n" + "Outputs a bunch of cube objects, mostly for testing" + ) + VVOX = BoolProperty( + name="Voxel (experimental)", + default=False, + description="Output to a voxel file to bpy.data.filepath\FSLGvoxels.raw\n" + "(doesn't work well right now)" + ) + IBOOL = BoolProperty( + name="Use Insulator object", + default=False, + description="Use insulator mesh object to prevent growth of bolt in areas" + ) + OOB = StringProperty( + name="Select", + default="", + description="Origin of bolt, can be an Empty\n" + "if object is a mesh will use all verts as charges") + GOB = StringProperty( + name="Select", + default="", + description="Object to use as ground plane, uses z coord only" + ) + COB = StringProperty( + name="Select", + default="", + description="Object to use as cloud, best to use a cube" + ) + IOB = StringProperty( + name="Select", + default="", + description="Object to use as insulator, 'voxelized'\n" + "before generating bolt (can be slow)" + ) + # object_mangle_tools properties + mangle_constraint_vector = BoolVectorProperty( + name="Mangle Constraint", + default=(True, True, True), + subtype='XYZ', + description="Constrains Mangle Direction" + ) + mangle_random_magnitude = IntProperty( + name="Mangle Severity", + default=5, + min=1, max=30, + description="Severity of mangling" + ) + mangle_name = StringProperty( + name="Shape Key Name", + default="mangle", + description="Name given for mangled shape keys" + ) + # unfold_transition properties + unfold_arm_name = StringProperty( + default="" + ) + unfold_modo = EnumProperty( + name="", + items=[("cursor", "3D Cursor", "Use the Distance to 3D Cursor"), + ("weight", "Weight Map", "Use a Painted Weight map"), + ("index", "Mesh Indices", "Use Faces and Vertices index")], + description="How to Sort Bones for animation", default="cursor" + ) + unfold_flip = BoolProperty( + name="Flipping Faces", + default=False, + description="Rotate faces around the Center and skip Scaling - " + "keep checked for both operators" + ) + unfold_fold_duration = IntProperty( + name="Total Time", + min=5, soft_min=25, + max=10000, soft_max=2500, + default=200, + description="Total animation length" + ) + unfold_sca_time = IntProperty( + name="Scale Time", + min=1, + max=5000, soft_max=500, + default=10, + description="Faces scaling time" + ) + unfold_rot_time = IntProperty( + name="Rotation Time", + min=1, soft_min=5, + max=5000, soft_max=500, + default=15, + description="Faces rotation time" + ) + unfold_rot_max = IntProperty( + name="Angle", + min=-180, + max=180, + default=135, + description="Faces rotation angle" + ) + unfold_fold_noise = IntProperty( + name="Noise", + min=0, + max=500, soft_max=50, + default=0, + description="Offset some faces animation" + ) + unfold_bounce = FloatProperty( + name="Bounce", + min=0, + max=10, soft_max=2.5, + default=0, + description="Add some bounce to rotation" + ) + unfold_from_point = BoolProperty( + name="Point", + default=False, + description="Scale faces from a Point instead of from an Edge" + ) + unfold_wiggle_rot = BoolProperty( + name="Wiggle", + default=False, + description="Use all Axis + Random Rotation instead of X Aligned" + ) + # oscurart_constellation + constellation_limit = FloatProperty( + name="Inital Threshold", + description="Edges will be created only if the distance\n" + "between vertices is smaller than this value\n" + "This is a starting value on Operator Invoke", + default=2, + min=0 + ) + + +# Class list +classes = ( + AdvancedObjPreferences1, + AdvancedObjProperties1, + ) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.advanced_objects1 = PointerProperty( + type=AdvancedObjProperties1 + ) + + prefs = get_addon_preferences() + for mod in sub_modules: + if not hasattr(mod, '__addon_enabled__'): + mod.__addon_enabled__ = False + name = mod.__name__.split('.')[-1] + if getattr(prefs, 'use_' + name): + register_submodule(mod) + + +def unregister(): + for mod in sub_modules: + if mod.__addon_enabled__: + unregister_submodule(mod) + del bpy.types.Scene.advanced_objects1 + + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + +if __name__ == "__main__": + register() diff --git a/add_advanced_objects_panels/delaunay_voronoi.py b/add_advanced_objects_panels/delaunay_voronoi.py new file mode 100644 index 00000000..ec8f330a --- /dev/null +++ b/add_advanced_objects_panels/delaunay_voronoi.py @@ -0,0 +1,312 @@ +# -*- coding:utf-8 -*- + +# ##### 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 ##### + +bl_info = { + "name": "Delaunay Voronoi", + "description": "Points cloud Delaunay triangulation in 2.5D " + "(suitable for terrain modelling) or Voronoi diagram in 2D", + "author": "Domlysz, Oscurart", + "version": (1, 3), + "blender": (2, 7, 0), + "location": "View3D > Tools > GIS", + "warning": "", + "wiki_url": "https://github.com/domlysz/BlenderGIS/wiki", + "category": "Add Mesh" + } + + + +import bpy +from .DelaunayVoronoi import ( + computeVoronoiDiagram, + computeDelaunayTriangulation, + ) +from bpy.types import ( + Operator, + Panel, + ) +from bpy.props import EnumProperty + + +# Globals +# set to True to enable debug_prints +DEBUG = False + + +def debug_prints(text=""): + global DEBUG + if DEBUG and text: + print(text) + + +class Point: + def __init__(self, x, y, z): + self.x, self.y, self.z = x, y, z + + +def unique(L): + """Return a list of unhashable elements in s, but without duplicates. + [[1, 2], [2, 3], [1, 2]] >>> [[1, 2], [2, 3]]""" + # For unhashable objects, you can sort the sequence and + # then scan from the end of the list, deleting duplicates as you go + nDupli = 0 + nZcolinear = 0 + # sort() brings the equal elements together; then duplicates + # are easy to weed out in a single pass + L.sort() + last = L[-1] + for i in range(len(L) - 2, -1, -1): + if last[:2] == L[i][:2]: # XY coordinates compararison + if last[2] == L[i][2]: # Z coordinates compararison + nDupli += 1 # duplicates vertices + else: # Z colinear + nZcolinear += 1 + del L[i] + else: + last = L[i] + # list data type is mutable, input list will automatically update + # and doesn't need to be returned + return (nDupli, nZcolinear) + + +def checkEqual(lst): + return lst[1:] == lst[:-1] + + +class ToolsPanelDelaunay(Panel): + bl_category = "Create" + bl_label = "Delaunay Voronoi" + bl_space_type = "VIEW_3D" + bl_context = "objectmode" + bl_region_type = "TOOLS" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + layout = self.layout + adv_obj = context.scene.advanced_objects + + box = layout.box() + col = box.column(align=True) + col.label("Point Cloud:") + col.operator("delaunay.triangulation") + col.operator("voronoi.tesselation") + + +class OBJECT_OT_TriangulateButton(Operator): + bl_idname = "delaunay.triangulation" + bl_label = "Triangulation" + bl_description = ("Terrain points cloud Delaunay triangulation in 2.5D\n" + "Needs an existing Active Mesh Object") + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + obj = context.active_object + return (obj is not None and obj.type == "MESH") + + def execute(self, context): + # move the check into the poll + obj = context.active_object + + # Get points coodinates + r = obj.rotation_euler + s = obj.scale + mesh = obj.data + vertsPts = [vertex.co for vertex in mesh.vertices] + + # Remove duplicate + verts = [[vert.x, vert.y, vert.z] for vert in vertsPts] + nDupli, nZcolinear = unique(verts) + nVerts = len(verts) + + debug_prints(text=str(nDupli) + " duplicate points ignored") + debug_prints(str(nZcolinear) + " z colinear points excluded") + + if nVerts < 3: + self.report({"WARNING"}, + "Not enough points to continue. Operation Cancelled") + + return {"CANCELLED"} + + # Check colinear + xValues = [pt[0] for pt in verts] + yValues = [pt[1] for pt in verts] + + if checkEqual(xValues) or checkEqual(yValues): + self.report({'ERROR'}, "Points are colinear") + return {'FINISHED'} + + # Triangulate + debug_prints(text="Triangulate " + str(nVerts) + " points...") + + vertsPts = [Point(vert[0], vert[1], vert[2]) for vert in verts] + triangles = computeDelaunayTriangulation(vertsPts) + # reverse point order --> if all triangles are specified anticlockwise then all faces up + triangles = [tuple(reversed(tri)) for tri in triangles] + + debug_prints(text=str(len(triangles)) + " triangles") + + # Create new mesh structure + debug_prints(text="Create mesh...") + tinMesh = bpy.data.meshes.new("TIN") # create a new mesh + tinMesh.from_pydata(verts, [], triangles) # Fill the mesh with triangles + tinMesh.update(calc_edges=True) # Update mesh with new data + + # Create an object with that mesh + tinObj = bpy.data.objects.new("TIN", tinMesh) + + # Place object + tinObj.location = obj.location.copy() + tinObj.rotation_euler = r + tinObj.scale = s + + # Update scene + bpy.context.scene.objects.link(tinObj) # Link object to scene + bpy.context.scene.objects.active = tinObj + tinObj.select = True + obj.select = False + + self.report({"INFO"}, + "Mesh created (" + str(len(triangles)) + " triangles)") + + return {'FINISHED'} + + +class OBJECT_OT_VoronoiButton(Operator): + bl_idname = "voronoi.tesselation" + bl_label = "Diagram" + bl_description = ("Points cloud Voronoi diagram in 2D\n" + "Needs an existing Active Mesh Object") + bl_options = {"REGISTER", "UNDO"} + + meshType = EnumProperty( + items=[('Edges', "Edges", "Edges Only - do not fill Faces"), + ('Faces', "Faces", "Fill Faces in the new Object")], + name="Mesh type", + description="Type of geometry to generate" + ) + + @classmethod + def poll(cls, context): + obj = context.active_object + return (obj is not None and obj.type == "MESH") + + def execute(self, context): + # move the check into the poll + obj = context.active_object + + # Get points coodinates + r = obj.rotation_euler + s = obj.scale + mesh = obj.data + vertsPts = [vertex.co for vertex in mesh.vertices] + + # Remove duplicate + verts = [[vert.x, vert.y, vert.z] for vert in vertsPts] + nDupli, nZcolinear = unique(verts) + nVerts = len(verts) + + debug_prints(text=str(nDupli) + " duplicates points ignored") + debug_prints(text=str(nZcolinear) + " z colinear points excluded") + + if nVerts < 3: + self.report({"WARNING"}, + "Not enough points to continue. Operation Cancelled") + + return {"CANCELLED"} + + # Check colinear + xValues = [pt[0] for pt in verts] + yValues = [pt[1] for pt in verts] + + if checkEqual(xValues) or checkEqual(yValues): + self.report({"WARNING"}, + "Points are colinear. Operation Cancelled") + + return {"CANCELLED"} + + # Create diagram + debug_prints(text="Tesselation... (" + str(nVerts) + " points)") + + xbuff, ybuff = 5, 5 + zPosition = 0 + vertsPts = [Point(vert[0], vert[1], vert[2]) for vert in verts] + + if self.meshType == "Edges": + pts, edgesIdx = computeVoronoiDiagram( + vertsPts, xbuff, ybuff, + polygonsOutput=False, formatOutput=True + ) + else: + pts, polyIdx = computeVoronoiDiagram( + vertsPts, xbuff, ybuff, polygonsOutput=True, + formatOutput=True, closePoly=False + ) + + pts = [[pt[0], pt[1], zPosition] for pt in pts] + + # Create new mesh structure + voronoiDiagram = bpy.data.meshes.new("VoronoiDiagram") # create a new mesh + + if self.meshType == "Edges": + # Fill the mesh with triangles + voronoiDiagram.from_pydata(pts, edgesIdx, []) + else: + # Fill the mesh with triangles + voronoiDiagram.from_pydata(pts, [], list(polyIdx.values())) + + voronoiDiagram.update(calc_edges=True) # Update mesh with new data + # create an object with that mesh + voronoiObj = bpy.data.objects.new("VoronoiDiagram", voronoiDiagram) + # place object + voronoiObj.location = obj.location.copy() + voronoiObj.rotation_euler = r + voronoiObj.scale = s + + # update scene + bpy.context.scene.objects.link(voronoiObj) # Link object to scene + bpy.context.scene.objects.active = voronoiObj + voronoiObj.select = True + obj.select = False + + # Report + if self.meshType == "Edges": + self.report({"INFO"}, "Mesh created (" + str(len(edgesIdx)) + " edges)") + else: + self.report({"INFO"}, "Mesh created (" + str(len(polyIdx)) + " polygons)") + + return {'FINISHED'} + +# Register +def register(): + bpy.utils.register_class(OBJECT_OT_VoronoiButton) + bpy.utils.register_class(OBJECT_OT_TriangulateButton) + bpy.utils.register_class(ToolsPanelDelaunay) + + +def unregister(): + bpy.utils.unregister_class(OBJECT_OT_VoronoiButton) + bpy.utils.unregister_class(OBJECT_OT_TriangulateButton) + bpy.utils.unregister_class(ToolsPanelDelaunay) + + +if __name__ == "__main__": + register() + diff --git a/add_advanced_objects_panels/drop_to_ground.py b/add_advanced_objects_panels/drop_to_ground.py new file mode 100644 index 00000000..35020020 --- /dev/null +++ b/add_advanced_objects_panels/drop_to_ground.py @@ -0,0 +1,347 @@ +# ##### 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 ##### + +bl_info = { + "name": "Drop to Ground1", + "author": "Unnikrishnan(kodemax), Florian Meyer(testscreenings)", + "blender": (2, 71, 0), + "location": "3D View > Toolshelf > Tools Tab", + "description": "Drop selected objects on active object", + "warning": "", + "category": "Object"} + + +import bpy +import bmesh +from mathutils import ( + Vector, + Matrix, + ) +from bpy.types import ( + Operator, + Panel, + ) +from bpy.props import BoolProperty + + +def get_align_matrix(location, normal): + up = Vector((0, 0, 1)) + angle = normal.angle(up) + axis = up.cross(normal) + mat_rot = Matrix.Rotation(angle, 4, axis) + mat_loc = Matrix.Translation(location) + mat_align = mat_rot * mat_loc + return mat_align + + +def transform_ground_to_world(sc, ground): + tmpMesh = ground.to_mesh(sc, True, 'PREVIEW') + tmpMesh.transform(ground.matrix_world) + tmp_ground = bpy.data.objects.new('tmpGround', tmpMesh) + sc.objects.link(tmp_ground) + sc.update() + + return tmp_ground + + +def get_lowest_world_co_from_mesh(ob, mat_parent=None): + bme = bmesh.new() + bme.from_mesh(ob.data) + mat_to_world = ob.matrix_world.copy() + if mat_parent: + mat_to_world = mat_parent * mat_to_world + lowest = None + for v in bme.verts: + if not lowest: + lowest = v + if (mat_to_world * v.co).z < (mat_to_world * lowest.co).z: + lowest = v + lowest_co = mat_to_world * lowest.co + bme.free() + + return lowest_co + + +def get_lowest_world_co(context, ob, mat_parent=None): + if ob.type == 'MESH': + return get_lowest_world_co_from_mesh(ob) + + elif ob.type == 'EMPTY' and ob.dupli_type == 'GROUP': + if not ob.dupli_group: + return None + + else: + lowest_co = None + for ob_l in ob.dupli_group.objects: + if ob_l.type == 'MESH': + lowest_ob_l = get_lowest_world_co_from_mesh(ob_l, ob.matrix_world) + if not lowest_co: + lowest_co = lowest_ob_l + if lowest_ob_l.z < lowest_co.z: + lowest_co = lowest_ob_l + + return lowest_co + + +def drop_objectsall(self, context): + ground = bpy.context.active_object + name = ground.name + + for obs in bpy.context.scene.objects: + obs.select = True + if obs.name == name: + obs.select = False + + obs2 = context.selected_objects + + tmp_ground = transform_ground_to_world(context.scene, ground) + down = Vector((0, 0, -10000)) + + for ob in obs2: + if self.use_origin: + lowest_world_co = ob.location + else: + lowest_world_co = get_lowest_world_co(context, ob) + + if not lowest_world_co: + message = "Type {} is not supported. Failed to drop {}".format(ob.type, ob.name) + self.reported.append(message) + continue + is_hit, hit_location, hit_normal, hit_index = tmp_ground.ray_cast(lowest_world_co, down) + + if not is_hit: + message = ob.name + " did not hit the Ground" + self.reported.append(message) + continue + + # simple drop down + to_ground_vec = hit_location - lowest_world_co + ob.location += to_ground_vec + + # drop with align to hit normal + if self.align: + to_center_vec = ob.location - hit_location # vec: hit_loc to origin + # rotate object to align with face normal + mat_normal = get_align_matrix(hit_location, hit_normal) + rot_euler = mat_normal.to_euler() + mat_ob_tmp = ob.matrix_world.copy().to_3x3() + mat_ob_tmp.rotate(rot_euler) + mat_ob_tmp = mat_ob_tmp.to_4x4() + ob.matrix_world = mat_ob_tmp + # move_object to hit_location + ob.location = hit_location + # move object above surface again + to_center_vec.rotate(rot_euler) + ob.location += to_center_vec + + # cleanup + bpy.ops.object.select_all(action='DESELECT') + tmp_ground.select = True + bpy.ops.object.delete('EXEC_DEFAULT') + for ob in obs2: + ob.select = True + ground.select = True + + +def drop_objects(self, context): + ground = context.object + obs = context.selected_objects + obs.remove(ground) + tmp_ground = transform_ground_to_world(context.scene, ground) + down = Vector((0, 0, -10000)) + + for ob in obs: + if self.use_origin: + lowest_world_co = ob.location + else: + lowest_world_co = get_lowest_world_co(context, ob) + + if not lowest_world_co: + message = "Type {} is not supported. Failed to drop {}".format(ob.type, ob.name) + self.reported.append(message) + continue + + is_hit, hit_location, hit_normal, hit_index = tmp_ground.ray_cast(lowest_world_co, down) + if not is_hit: + message = ob.name + " did not hit the Ground" + self.reported.append(message) + continue + + # simple drop down + to_ground_vec = hit_location - lowest_world_co + ob.location += to_ground_vec + + # drop with align to hit normal + if self.align: + to_center_vec = ob.location - hit_location # vec: hit_loc to origin + # rotate object to align with face normal + mat_normal = get_align_matrix(hit_location, hit_normal) + rot_euler = mat_normal.to_euler() + mat_ob_tmp = ob.matrix_world.copy().to_3x3() + mat_ob_tmp.rotate(rot_euler) + mat_ob_tmp = mat_ob_tmp.to_4x4() + ob.matrix_world = mat_ob_tmp + # move_object to hit_location + ob.location = hit_location + # move object above surface again + to_center_vec.rotate(rot_euler) + ob.location += to_center_vec + + # cleanup + bpy.ops.object.select_all(action='DESELECT') + tmp_ground.select = True + bpy.ops.object.delete('EXEC_DEFAULT') + for ob in obs: + ob.select = True + ground.select = True + + +class OBJECT_OT_drop_to_ground(Operator): + bl_idname = "object.drop_on_active" + bl_label = "Drop to Ground" + bl_description = "Drop selected objects on the active object" + bl_options = {'REGISTER', 'UNDO'} + + align = BoolProperty( + name="Align to ground", + description="Aligns the object to the ground", + default=True) + use_origin = BoolProperty( + name="Use Center", + description="Drop to objects origins", + default=False) + reported = [] + + @classmethod + def poll(cls, context): + return len(context.selected_objects) >= 2 + + def execute(self, context): + drop_objects(self, context) + + if self.reported: + self.report({"INFO"}, + "Operation failed on some objects. See the Console for more Info") + report_items = " \n".join(self.reported) + print("\n[Drop to Ground Report]\n{}\n".format(report_items)) + + self.reported = [] + + return {'FINISHED'} + + +class OBJECT_OT_drop_all_ground(Operator): + bl_idname = "object.drop_all_active" + bl_label = "Drop All to Ground (Active Object)" + bl_description = "Drop selected objects on active object" + bl_options = {'REGISTER', 'UNDO'} + + align = BoolProperty( + name="Align to ground", + description="Aligns the object to the ground", + default=True) + use_origin = BoolProperty( + name="Use Center", + description="Drop to objects origins", + default=False) + reported = [] + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def execute(self, context): + drop_objectsall(self, context) + + if self.reported: + self.report({"INFO"}, + "Operation failed on some objects. See the Console for more Info") + report_items = " \n".join(self.reported) + print("\n[Drop All to Ground Report]\n{}\n".format(report_items)) + + self.reported = [] + + return {'FINISHED'} + + +class Drop_help(Operator): + bl_idname = "help.drop" + bl_label = "" + + is_all = BoolProperty( + default=True, + options={"HIDDEN"} + ) + + def draw(self, context): + layout = self.layout + layout.label("To use:") + + if self.is_all is False: + layout.label("Name the base object 'Ground'") + layout.label("Select the object's to drop") + layout.label("Then Shift Select 'Ground'") + else: + layout.label("Select the ground mesh and press Drop all") + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_popup(self, width=300) + + +class Drop_Operator_Panel(Panel): + bl_label = "Drop To Ground" + bl_region_type = "TOOLS" + bl_space_type = "VIEW_3D" + bl_options = {'DEFAULT_CLOSED'} + bl_context = "objectmode" + bl_category = "Create" + + def draw(self, context): + layout = self.layout + + row = layout.split(percentage=0.8, align=True) + row.operator(OBJECT_OT_drop_to_ground.bl_idname, + text="Drop Selected") + row.operator("help.drop", icon="LAYER_USED").is_all = False + + row = layout.split(percentage=0.8, align=True) + row.operator(OBJECT_OT_drop_all_ground.bl_idname, + text="Drop All") + row.operator("help.drop", icon="LAYER_USED").is_all = True + + +# Register +def register(): + bpy.utils.register_class(OBJECT_OT_drop_all_ground) + bpy.utils.register_class(OBJECT_OT_drop_to_ground) + bpy.utils.register_class(Drop_Operator_Panel) + bpy.utils.register_class(Drop_help) + + +def unregister(): + bpy.utils.unregister_class(OBJECT_OT_drop_all_ground) + bpy.utils.unregister_class(OBJECT_OT_drop_to_ground) + bpy.utils.unregister_class(Drop_Operator_Panel) + bpy.utils.unregister_class(Drop_help) + + +if __name__ == "__main__": + register() diff --git a/add_advanced_objects_panels/object_laplace_lightning.py b/add_advanced_objects_panels/object_laplace_lightning.py new file mode 100644 index 00000000..2f3b2498 --- /dev/null +++ b/add_advanced_objects_panels/object_laplace_lightning.py @@ -0,0 +1,1440 @@ +# ##### 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 ##### + +# NOTE: moved the winmgr properties to __init__ and scene +# search for context.scene.advanced_objects + +bl_info = { + "name": "Laplacian Lightning", + "author": "teldredge", + "blender": (2, 78, 0), + "location": "View3D > Toolshelf > Create Tab", + "description": "Lightning mesh generator using laplacian growth algorithm", + "warning": "", + "category": "Object"} + +# BLENDER LAPLACIAN LIGHTNING +# teldredge +# www.funkboxing.com +# https://developer.blender.org/T27189 + +# using algorithm from +# FAST SIMULATION OF LAPLACIAN GROWTH (FSLG) +# http://gamma.cs.unc.edu/FRAC/ + +# and a few ideas ideas from +# FAST ANIMATION OF LIGHTNING USING AN ADAPTIVE MESH (FALUAM) +# http://gamma.cs.unc.edu/FAST_LIGHTNING/ + + +""" +----- RELEASE LOG/NOTES/PONTIFICATIONS ----- +v0.1.0 - 04.11.11 + basic generate functions and UI + object creation report (Custom Properties: FSLG_REPORT) +v0.2.0 - 04.15.11 + started spelling laplacian right. + add curve function (not in UI) ...twisting problem + classify stroke by MAIN path, h-ORDER paths, TIP paths + jitter cells for mesh creation + add materials if present +v0.2.1 - 04.16.11 + mesh classification speedup +v0.2.2 - 04.21.11 + fxns to write/read array to file + restrict growth to insulator cells (object bounding box) + origin/ground defineable by object + gridunit more like 'resolution' +v0.2.3 - 04.24.11 + cloud attractor object (termintates loop if hit) + secondary path orders (hOrder) disabled in UI (set to 1) +v0.2.4 - 04.26.11 + fixed object selection in UI + will not run if required object not selected + moved to view 3d > toolbox +v0.2.5 - 05.08.11 + testing for 2.57b + single mesh output (for build modifier) + speedups (dist fxn) +v0.2.6 - 06.20.11 + scale/pos on 'write to cubes' works now + if origin obj is mesh, uses all verts as initial charges + semi-helpful tooltips + speedups, faster dedupe fxn, faster classification + use any shape mesh obj as insulator mesh + must have rot=0, scale=1, origin set to geometry + often fails to block bolt with curved/complex shapes + separate single and multi mesh creation +v0.2.7 - 01.05.13 + fixed the issue that prevented enabling the add-on + fixed makeMeshCube fxn + disabled visualization for voxels + +v0.x - + -prevent create_setup_objects from generating duplicates + -fix vis fxn to only buildCPGraph once for VM or VS + -improve list fxns (rid of ((x,y,z),w) and use (x,y,z,w)), use 'sets' + -create python cmodule for a few of most costly fxns + i have pretty much no idea how to do this yet + -cloud and insulator can be groups of MESH objs + -text output, possibly to save on interrupt, allow continue from text + -?hook modifiers from tips->sides->main, weight w/ vert groups + -user defined 'attractor' path + -fix add curve function + -animated arcs via. ionization path + -environment map boundary conditions - requires Eqn. 15 from FSLG. + -assign wattage at each segment for HDRI + -?default settings for -lightning, -teslacoil, -spark/arc + -fix hOrder functionality + -multiple 'MAIN' brances for non-lightning discharges + -n-symmetry option, create mirror images, snowflakes, etc... +""" + +import bpy +import time +import random +from bpy.types import ( + Operator, + Panel, + ) +# from math import sqrt +from mathutils import Vector +import struct +import bisect +import os.path + +# -- Globals -- +notZero = 0.0000000001 +# set to True to enable debug prints +DEBUG = False + + +# Utility Functions + +# func - function name, text - message, var - variable to print +# it can have one variable to observe +def debug_prints(func="", text="Message", var=None): + global DEBUG + if DEBUG: + print("\n[{}]\nmessage: {}".format(func, text)) + if var: + print("variable: ", var) + + +# pass variables just like for the regular prints +def debug_print_vars(*args, **kwargs): + global DEBUG + if DEBUG: + print(*args, **kwargs) + + +def within(x, y, d): + # CHECK IF x - d <= y <= x + d + if x - d <= y and x + d >= y: + return True + else: + return False + + +def dist(ax, ay, az, bx, by, bz): + dv = Vector((ax, ay, az)) - Vector((bx, by, bz)) + d = dv.length + return d + + +def splitList(aList, idx): + ll = [] + for x in aList: + ll.append(x[idx]) + return ll + + +def splitListCo(aList): + ll = [] + for p in aList: + ll.append((p[0], p[1], p[2])) + return ll + + +def getLowHigh(aList): + tLow = aList[0] + tHigh = aList[0] + for a in aList: + if a < tLow: + tLow = a + if a > tHigh: + tHigh = a + return tLow, tHigh + + +def weightedRandomChoice(aList): + tL = [] + tweight = 0 + for a in range(len(aList)): + idex = a + weight = aList[a] + if weight > 0.0: + tweight += weight + tL.append((tweight, idex)) + i = bisect.bisect(tL, (random.uniform(0, tweight), None)) + r = tL[i][1] + return r + + +def getStencil3D_26(x, y, z): + nL = [] + for xT in range(x - 1, x + 2): + for yT in range(y - 1, y + 2): + for zT in range(z - 1, z + 2): + nL.append((xT, yT, zT)) + nL.remove((x, y, z)) + return nL + + +def jitterCells(aList, jit): + j = jit / 2 + bList = [] + for a in aList: + ax = a[0] + random.uniform(-j, j) + ay = a[1] + random.uniform(-j, j) + az = a[2] + random.uniform(-j, j) + bList.append((ax, ay, az)) + return bList + + +def deDupe(seq, idfun=None): + # Thanks to this guy - http://www.peterbe.com/plog/uniqifiers-benchmark + if idfun is None: + def idfun(x): + return x + seen = {} + result = [] + for item in seq: + marker = idfun(item) + if marker in seen: + continue + seen[marker] = 1 + result.append(item) + return result + + +# Visulization functions + +def writeArrayToVoxel(arr, filename): + gridS = 64 + half = int(gridS / 2) + bitOn = 255 + aGrid = [[[0 for z in range(gridS)] for y in range(gridS)] for x in range(gridS)] + for a in arr: + try: + aGrid[a[0] + half][a[1] + half][a[2] + half] = bitOn + except: + debug_prints(func="writeArrayToVoxel", text="Particle beyond voxel domain") + + file = open(filename, "wb") + for z in range(gridS): + for y in range(gridS): + for x in range(gridS): + file.write(struct.pack('B', aGrid[x][y][z])) + file.flush() + file.close() + + +def writeArrayToFile(arr, filename): + file = open(filename, "w") + for a in arr: + tstr = str(a[0]) + ',' + str(a[1]) + ',' + str(a[2]) + '\n' + file.write(tstr) + file.close + + +def readArrayFromFile(filename): + file = open(filename, "r") + arr = [] + for f in file: + pt = f[0:-1].split(',') + arr.append((int(pt[0]), int(pt[1]), int(pt[2]))) + return arr + + +def makeMeshCube_OLD(msize): + msize = msize / 2 + mmesh = bpy.data.meshes.new('q') + mmesh.vertices.add(8) + mmesh.vertices[0].co = [-msize, -msize, -msize] + mmesh.vertices[1].co = [-msize, msize, -msize] + mmesh.vertices[2].co = [msize, msize, -msize] + mmesh.vertices[3].co = [msize, -msize, -msize] + mmesh.vertices[4].co = [-msize, -msize, msize] + mmesh.vertices[5].co = [-msize, msize, msize] + mmesh.vertices[6].co = [msize, msize, msize] + mmesh.vertices[7].co = [msize, -msize, msize] + mmesh.faces.add(6) + mmesh.faces[0].vertices_raw = [0, 1, 2, 3] + mmesh.faces[1].vertices_raw = [0, 4, 5, 1] + mmesh.faces[2].vertices_raw = [2, 1, 5, 6] + mmesh.faces[3].vertices_raw = [3, 2, 6, 7] + mmesh.faces[4].vertices_raw = [0, 3, 7, 4] + mmesh.faces[5].vertices_raw = [5, 4, 7, 6] + mmesh.update(calc_edges=True) + + return(mmesh) + + +def makeMeshCube(msize): + m2 = msize / 2 + # verts = [(0,0,0),(0,5,0),(5,5,0),(5,0,0),(0,0,5),(0,5,5),(5,5,5),(5,0,5)] + verts = [(-m2, -m2, -m2), (-m2, m2, -m2), (m2, m2, -m2), (m2, -m2, -m2), + (-m2, -m2, m2), (-m2, m2, m2), (m2, m2, m2), (m2, -m2, m2)] + faces = [ + (0, 1, 2, 3), (4, 5, 6, 7), (0, 4, 5, 1), + (1, 5, 6, 2), (2, 6, 7, 3), (3, 7, 4, 0) + ] + # Define mesh and object + mmesh = bpy.data.meshes.new("Cube") + + # Create mesh + mmesh.from_pydata(verts, [], faces) + mmesh.update(calc_edges=True) + return(mmesh) + + +def writeArrayToCubes(arr, gridBU, orig, cBOOL=False, jBOOL=True): + for a in arr: + x = a[0] + y = a[1] + z = a[2] + me = makeMeshCube(gridBU) + ob = bpy.data.objects.new('xCUBE', me) + ob.location.x = (x * gridBU) + orig[0] + ob.location.y = (y * gridBU) + orig[1] + ob.location.z = (z * gridBU) + orig[2] + + if cBOOL: # mostly unused + # pos + blue, neg - red, zero: black + col = (1.0, 1.0, 1.0, 1.0) + if a[3] == 0: + col = (0.0, 0.0, 0.0, 1.0) + if a[3] < 0: + col = (-a[3], 0.0, 0.0, 1.0) + if a[3] > 0: + col = (0.0, 0.0, a[3], 1.0) + ob.color = col + bpy.context.scene.objects.link(ob) + bpy.context.scene.update() + + if jBOOL: + # Selects all cubes w/ ?bpy.ops.object.join() b/c + # Can't join all cubes to a single mesh right... argh... + for q in bpy.context.scene.objects: + q.select = False + if q.name[0:5] == 'xCUBE': + q.select = True + bpy.context.scene.objects.active = q + + +def addVert(ob, pt, conni=-1): + mmesh = ob.data + mmesh.vertices.add(1) + vcounti = len(mmesh.vertices) - 1 + mmesh.vertices[vcounti].co = [pt[0], pt[1], pt[2]] + if conni > -1: + mmesh.edges.add(1) + ecounti = len(mmesh.edges) - 1 + mmesh.edges[ecounti].vertices = [conni, vcounti] + mmesh.update() + + +def addEdge(ob, va, vb): + mmesh = ob.data + mmesh.edges.add(1) + ecounti = len(mmesh.edges) - 1 + mmesh.edges[ecounti].vertices = [va, vb] + mmesh.update() + + +def newMesh(mname): + mmesh = bpy.data.meshes.new(mname) + omesh = bpy.data.objects.new(mname, mmesh) + bpy.context.scene.objects.link(omesh) + return omesh + + +def writeArrayToMesh(mname, arr, gridBU, rpt=None): + mob = newMesh(mname) + mob.scale = (gridBU, gridBU, gridBU) + if rpt: + addReportProp(mob, rpt) + addVert(mob, arr[0], -1) + for ai in range(1, len(arr)): + a = arr[ai] + addVert(mob, a, ai - 1) + return mob + + +# out of order - some problem with it adding (0,0,0) +def writeArrayToCurves(cname, arr, gridBU, bd=.05, rpt=None): + cur = bpy.data.curves.new('fslg_curve', 'CURVE') + cur.use_fill_front = False + cur.use_fill_back = False + cur.bevel_depth = bd + cur.bevel_resolution = 2 + cob = bpy.data.objects.new(cname, cur) + cob.scale = (gridBU, gridBU, gridBU) + + if rpt: + addReportProp(cob, rpt) + bpy.context.scene.objects.link(cob) + cur.splines.new('BEZIER') + cspline = cur.splines[0] + div = 1 # spacing for handles (2 - 1/2 way, 1 - next bezier) + + for a in range(len(arr)): + cspline.bezier_points.add(1) + bp = cspline.bezier_points[len(cspline.bezier_points) - 1] + if a - 1 < 0: + hL = arr[a] + else: + hx = arr[a][0] - ((arr[a][0] - arr[a - 1][0]) / div) + hy = arr[a][1] - ((arr[a][1] - arr[a - 1][1]) / div) + hz = arr[a][2] - ((arr[a][2] - arr[a - 1][2]) / div) + hL = (hx, hy, hz) + + if a + 1 > len(arr) - 1: + hR = arr[a] + else: + hx = arr[a][0] + ((arr[a + 1][0] - arr[a][0]) / div) + hy = arr[a][1] + ((arr[a + 1][1] - arr[a][1]) / div) + hz = arr[a][2] + ((arr[a + 1][2] - arr[a][2]) / div) + hR = (hx, hy, hz) + bp.co = arr[a] + bp.handle_left = hL + bp.handle_right = hR + + +def addArrayToMesh(mob, arr): + addVert(mob, arr[0], -1) + mmesh = mob.data + vcounti = len(mmesh.vertices) - 1 + for ai in range(1, len(arr)): + a = arr[ai] + addVert(mob, a, len(mmesh.vertices) - 1) + + +def addMaterial(ob, matname): + mat = bpy.data.materials[matname] + ob.active_material = mat + + +def writeStokeToMesh(arr, jarr, MAINi, HORDERi, TIPSi, orig, gs, rpt=None): + # main branch + debug_prints(func="writeStokeToMesh", text='Writing main branch') + llmain = [] + + for x in MAINi: + llmain.append(jarr[x]) + mob = writeArrayToMesh('la0MAIN', llmain, gs) + mob.location = orig + + # horder branches + for hOi in range(len(HORDERi)): + debug_prints(func="writeStokeToMesh", text="Writing order", var=hOi) + hO = HORDERi[hOi] + hob = newMesh('la1H' + str(hOi)) + + for y in hO: + llHO = [] + for x in y: + llHO.append(jarr[x]) + addArrayToMesh(hob, llHO) + hob.scale = (gs, gs, gs) + hob.location = orig + + # tips + debug_prints(func="writeStokeToMesh", text="Writing tip paths") + tob = newMesh('la2TIPS') + for y in TIPSi: + llt = [] + for x in y: + llt.append(jarr[x]) + addArrayToMesh(tob, llt) + tob.scale = (gs, gs, gs) + tob.location = orig + + # add materials to objects (if they exist) + try: + addMaterial(mob, 'edgeMAT-h0') + addMaterial(hob, 'edgeMAT-h1') + addMaterial(tob, 'edgeMAT-h2') + debug_prints(func="writeStokeToMesh", text="Added materials") + + except: + debug_prints(func="writeStokeToMesh", text="Materials not found") + + # add generation report to all meshes + if rpt: + addReportProp(mob, rpt) + addReportProp(hob, rpt) + addReportProp(tob, rpt) + + +def writeStokeToSingleMesh(arr, jarr, orig, gs, mct, rpt=None): + sgarr = buildCPGraph(arr, mct) + llALL = [] + + Aob = newMesh('laALL') + for pt in jarr: + addVert(Aob, pt) + for cpi in range(len(sgarr)): + ci = sgarr[cpi][0] + pi = sgarr[cpi][1] + addEdge(Aob, pi, ci) + Aob.location = orig + Aob.scale = ((gs, gs, gs)) + + if rpt: + addReportProp(Aob, rpt) + + +def visualizeArray(cg, oob, gs, vm, vs, vc, vv, rst): + winmgr = bpy.context.scene.advanced_objects + # IN: (cellgrid, origin, gridscale, + # mulimesh, single mesh, cubes, voxels, report sting) + origin = oob.location + + # deal with vert multi-origins + oct = 2 + if oob.type == 'MESH': + oct = len(oob.data.vertices) + + # jitter cells + if vm or vs: + cjarr = jitterCells(cg, 1) + + if vm: # write array to multi mesh + + aMi, aHi, aTi = classifyStroke(cg, oct, winmgr.HORDER) + debug_prints(func="visualizeArray", text="Writing to multi-mesh") + writeStokeToMesh(cg, cjarr, aMi, aHi, aTi, origin, gs, rst) + debug_prints(func="visualizeArray", text="Multi-mesh written") + + if vs: # write to single mesh + debug_prints(func="visualizeArray", text="Writing to single mesh") + writeStokeToSingleMesh(cg, cjarr, origin, gs, oct, rst) + debug_prints(func="visualizeArray", text="Single mesh written") + + if vc: # write array to cube objects + debug_prints(func="visualizeArray", text="Writing to cubes") + writeArrayToCubes(cg, gs, origin) + debug_prints(func="visualizeArray", text="Cubes written") + + if vv: # write array to voxel data file + debug_prints(func="visualizeArray", text="Writing to voxels") + fname = "FSLGvoxels.raw" + path = os.path.dirname(bpy.data.filepath) + writeArrayToVoxel(cg, path + "\\" + fname) + + debug_prints(func="visualizeArray", + text="Voxel data written to:", var=path + "\\" + fname) + + # read/write array to file (might not be necessary) + # tfile = 'c:\\testarr.txt' + # writeArrayToFile(cg, tfile) + # cg = readArrayFromFile(tfile) + + # read/write array to curves (out of order) + # writeArrayToCurves('laMAIN', llmain, .10, .25) + + +# Algorithm functions +# from faluam paper +# plus some stuff i made up + +def buildCPGraph(arr, sti=2): + # in -xyz array as built by generator + # out -[(childindex, parentindex)] + # sti - start index, 2 for empty, len(me.vertices) for mesh + sgarr = [] + sgarr.append((1, 0)) + + for ai in range(sti, len(arr)): + cs = arr[ai] + cpts = arr[0:ai] + cslap = getStencil3D_26(cs[0], cs[1], cs[2]) + + for nc in cslap: + ct = cpts.count(nc) + if ct > 0: + cti = cpts.index(nc) + sgarr.append((ai, cti)) + + return sgarr + + +def buildCPGraph_WORKINPROGRESS(arr, sti=2): + # in -xyz array as built by generator + # out -[(childindex, parentindex)] + # sti - start index, 2 for empty, len(me.vertices) for mesh + sgarr = [] + sgarr.append((1, 0)) + ctix = 0 + for ai in range(sti, len(arr)): + cs = arr[ai] + # cpts = arr[0:ai] + cpts = arr[ctix:ai] + cslap = getStencil3D_26(cs[0], cs[1], cs[2]) + for nc in cslap: + ct = cpts.count(nc) + if ct > 0: + # cti = cpts.index(nc) + cti = ctix + cpts.index(nc) + ctix = cpts.index(nc) + + sgarr.append((ai, cti)) + + return sgarr + + +def findChargePath(oc, fc, ngraph, restrict=[], partial=True): + # oc -origin charge index, fc -final charge index + # ngraph -node graph, restrict- index of sites cannot traverse + # partial -return partial path if restriction encounterd + cList = splitList(ngraph, 0) + pList = splitList(ngraph, 1) + aRi = [] + cNODE = fc + for x in range(len(ngraph)): + pNODE = pList[cList.index(cNODE)] + aRi.append(cNODE) + cNODE = pNODE + npNODECOUNT = cList.count(pNODE) + if cNODE == oc: # stop if origin found + aRi.append(cNODE) # return path + return aRi + if npNODECOUNT == 0: # stop if no parents + return [] # return [] + if pNODE in restrict: # stop if parent is in restriction + if partial: # return partial or [] + aRi.append(cNODE) + return aRi + else: + return [] + + +def findTips(arr): + lt = [] + for ai in arr[0: len(arr) - 1]: + a = ai[0] + cCOUNT = 0 + for bi in arr: + b = bi[1] + if a == b: + cCOUNT += 1 + if cCOUNT == 0: + lt.append(a) + + return lt + + +def findChannelRoots(path, ngraph, restrict=[]): + roots = [] + for ai in range(len(ngraph)): + chi = ngraph[ai][0] + par = ngraph[ai][1] + if par in path and chi not in path and chi not in restrict: + roots.append(par) + droots = deDupe(roots) + + return droots + + +def findChannels(roots, tips, ngraph, restrict): + cPATHS = [] + for ri in range(len(roots)): + r = roots[ri] + sL = 1 + sPATHi = [] + for ti in range(len(tips)): + t = tips[ti] + if t < r: + continue + tPATHi = findChargePath(r, t, ngraph, restrict, False) + tL = len(tPATHi) + if tL > sL: + if countChildrenOnPath(tPATHi, ngraph) > 1: + sL = tL + sPATHi = tPATHi + tTEMP = t + tiTEMP = ti + if len(sPATHi) > 0: + debug_print_vars( + "\n[findChannels]\n", + "found path/idex from", ri, 'of', + len(roots), "possible | tips:", tTEMP, tiTEMP + ) + cPATHS.append(sPATHi) + tips.remove(tTEMP) + + return cPATHS + + +def findChannels_WORKINPROGRESS(roots, ttips, ngraph, restrict): + cPATHS = [] + tips = list(ttips) + for ri in range(len(roots)): + r = roots[ri] + sL = 1 + sPATHi = [] + tipREMOVE = [] # checked tip indexes, to be removed for next loop + for ti in range(len(tips)): + t = tips[ti] + if ti < ri: + continue + + tPATHi = findChargePath(r, t, ngraph, restrict, False) + tL = len(tPATHi) + if tL > sL: + if countChildrenOnPath(tPATHi, ngraph) > 1: + sL = tL + sPATHi = tPATHi + tTEMP = t + tiTEMP = ti + if tL > 0: + tipREMOVE.append(t) + if len(sPATHi) > 0: + debug_print_vars( + "\n[findChannels_WORKINPROGRESS]\n", + "found path from root idex", ri, 'of', + len(roots), "possible roots | of tips= ", len(tips) + ) + cPATHS.append(sPATHi) + + for q in tipREMOVE: + tips.remove(q) + + return cPATHS + + +def countChildrenOnPath(aPath, ngraph, quick=True): + # return how many branches + # count when node is a parent >1 times + # quick -stop and return after first + cCOUNT = 0 + pList = splitList(ngraph, 1) + + for ai in range(len(aPath) - 1): + ap = aPath[ai] + pc = pList.count(ap) + + if quick and pc > 1: + return pc + + return cCOUNT + + +# classify channels into 'main', 'hORDER/secondary' and 'side' +def classifyStroke(sarr, mct, hORDER=1): + debug_prints(func="classifyStroke", text="Classifying stroke") + # build child/parent graph (indexes of sarr) + sgarr = buildCPGraph(sarr, mct) + + # find main channel + debug_prints(func="classifyStroke", text="Finding MAIN") + oCharge = sgarr[0][1] + fCharge = sgarr[len(sgarr) - 1][0] + aMAINi = findChargePath(oCharge, fCharge, sgarr) + + # find tips + debug_prints(func="classifyStroke", text="Finding TIPS") + aTIPSi = findTips(sgarr) + + # find horder channel roots + # hcount = orders bewteen main and side/tips + # !!!still buggy!!! + hRESTRICT = list(aMAINi) # add to this after each time + allHPATHSi = [] # all ho paths: [[h0], [h1]...] + curPATHSi = [aMAINi] # list of paths find roots on + + for h in range(hORDER): + allHPATHSi.append([]) + for pi in range(len(curPATHSi)): # loop through all paths in this order + p = curPATHSi[pi] + # get roots for this path + aHROOTSi = findChannelRoots(p, sgarr, hRESTRICT) + debug_print_vars( + "\n[classifyStroke]\n", + "found", len(aHROOTSi), "roots in ORDER", h, ":paths:", len(curPATHSi) + ) + # get channels for these roots + if len(aHROOTSi) == 0: + debug_prints(func="classifyStroke", text="No roots for found for channel") + aHPATHSi = [] + continue + else: + aHPATHSiD = findChannels(aHROOTSi, aTIPSi, sgarr, hRESTRICT) + aHPATHSi = aHPATHSiD + allHPATHSi[h] += aHPATHSi + # set these channels as restrictions for next iterations + for hri in aHPATHSi: + hRESTRICT += hri + curPATHSi = aHPATHSi + + # side branches, final order of heirarchy + # from tips that are not in an existing path + # back to any other point that is already on a path + aDRAWNi = [] + aDRAWNi += aMAINi + for oH in allHPATHSi: + for o in oH: + aDRAWNi += o + aTPATHSi = [] + for a in aTIPSi: + if a not in aDRAWNi: + aPATHi = findChargePath(oCharge, a, sgarr, aDRAWNi) + aDRAWNi += aPATHi + aTPATHSi.append(aPATHi) + + return aMAINi, allHPATHSi, aTPATHSi + + +def voxelByVertex(ob, gs): + # 'voxelizes' verts in a mesh to list [(x,y,z),(x,y,z)] + # w/ respect gscale and ob origin (b/c should be origin obj) + # orig = ob.location + ll = [] + for v in ob.data.vertices: + x = int(v.co.x / gs) + y = int(v.co.y / gs) + z = int(v.co.z / gs) + ll.append((x, y, z)) + + return ll + + +def voxelByRays(ob, orig, gs): + # mesh into a 3dgrid w/ respect gscale and bolt origin + # - does not take object rotation/scale into account + # - this is a horrible, inefficient function + # maybe the raycast/grid thing are a bad idea. but i + # have to 'voxelize the object w/ resct to gscale/origin + bbox = ob.bound_box + bbxL = bbox[0][0] + bbxR = bbox[4][0] + bbyL = bbox[0][1] + bbyR = bbox[2][1] + bbzL = bbox[0][2] + bbzR = bbox[1][2] + xct = int((bbxR - bbxL) / gs) + yct = int((bbyR - bbyL) / gs) + zct = int((bbzR - bbzL) / gs) + xs = int(xct / 2) + ys = int(yct / 2) + zs = int(zct / 2) + + debug_print_vars( + "\n[voxelByRays]\n", + "Casting", xct, '/', yct, '/', zct, 'cells, total:', + xct * yct * zct, 'in obj-', ob.name + ) + ll = [] + rc = 100 # distance to cast from + # raycast top/bottom + debug_prints(func="voxelByRays", text="Raycasting top/bottom") + + for x in range(xct): + for y in range(yct): + xco = bbxL + (x * gs) + yco = bbyL + (y * gs) + v1 = ((xco, yco, rc)) + v2 = ((xco, yco, -rc)) + vz1 = ob.ray_cast(v1, v2) + vz2 = ob.ray_cast(v2, v1) + + debug_print_vars( + "\n[voxelByRays]\n", "vz1 is: ", vz1, "\nvz2 is: ", vz2 + ) + # Note: the API raycast return has changed now it is + # (result, location, normal, index) - result is a boolean + if vz1[0] is True: + ll.append((x - xs, y - ys, int(vz1[1][2] * (1 / gs)))) + if vz2[0] is True: + ll.append((x - xs, y - ys, int(vz2[1][2] * (1 / gs)))) + + # raycast front/back + debug_prints(func="voxelByRays", text="Raycasting front/back") + + for x in range(xct): + for z in range(zct): + xco = bbxL + (x * gs) + zco = bbzL + (z * gs) + v1 = ((xco, rc, zco)) + v2 = ((xco, -rc, zco)) + vy1 = ob.ray_cast(v1, v2) + vy2 = ob.ray_cast(v2, v1) + if vy1[0] is True: + ll.append((x - xs, int(vy1[1][1] * (1 / gs)), z - zs)) + if vy2[0] is True: + ll.append((x - xs, int(vy2[1][1] * (1 / gs)), z - zs)) + + # raycast left/right + debug_prints(func="voxelByRays", text="Raycasting left/right") + + for y in range(yct): + for z in range(zct): + yco = bbyL + (y * gs) + zco = bbzL + (z * gs) + v1 = ((rc, yco, zco)) + v2 = ((-rc, yco, zco)) + vx1 = ob.ray_cast(v1, v2) + vx2 = ob.ray_cast(v2, v1) + if vx1[0] is True: + ll.append((int(vx1[1][0] * (1 / gs)), y - ys, z - zs)) + if vx2[0] is True: + ll.append((int(vx2[1][0] * (1 / gs)), y - ys, z - zs)) + + # add in neighbors so bolt wont go through + nlist = [] + for l in ll: + nl = getStencil3D_26(l[0], l[1], l[2]) + nlist += nl + + # dedupe + debug_prints(func="voxelByRays", text="Added neighbors, deduping...") + rlist = deDupe(ll + nlist) + qlist = [] + + # relocate grid w/ respect gscale and bolt origin + # !!!need to add in obj rot/scale here somehow... + od = Vector( + ((ob.location[0] - orig[0]) / gs, + (ob.location[1] - orig[1]) / gs, + (ob.location[2] - orig[2]) / gs) + ) + for r in rlist: + qlist.append((r[0] + int(od[0]), r[1] + int(od[1]), r[2] + int(od[2]))) + + return qlist + + +def fakeGroundChargePlane(z, charge): + eCL = [] + xy = abs(z) / 2 + eCL += [(0, 0, z, charge)] + eCL += [(xy, 0, z, charge)] + eCL += [(0, xy, z, charge)] + eCL += [(-xy, 0, z, charge)] + eCL += [(0, -xy, z, charge)] + + return eCL + + +def addCharges(ll, charge): + # in: ll - [(x,y,z), (x,y,z)], charge - w + # out clist - [(x,y,z,w), (x,y,z,w)] + clist = [] + for l in ll: + clist.append((l[0], l[1], l[2], charge)) + return clist + + +# algorithm functions # +# from fslg # + +def getGrowthProbability_KEEPFORREFERENCE(uN, aList): + # in: un -user term, clist -candidate sites, olist -candidate site charges + # out: list of [(xyz), pot, prob] + cList = splitList(aList, 0) + oList = splitList(aList, 1) + Omin, Omax = getLowHigh(oList) + if Omin == Omax: + Omax += notZero + Omin -= notZero + PdL = [] + E = 0 + E = notZero # divisor for (fslg - eqn. 12) + for o in oList: + Uj = (o - Omin) / (Omax - Omin) # (fslg - eqn. 13) + E += pow(Uj, uN) + for oi in range(len(oList)): + o = oList[oi] + Ui = (o - Omin) / (Omax - Omin) + Pd = (pow(Ui, uN)) / E # (fslg - eqn. 12) + PdINT = Pd * 100 + PdL.append(Pd) + + return PdL + + +# work in progress, trying to speed these up +def fslg_e13(x, min, max, u): + return pow((x - min) / (max - min), u) + + +def addit(x, y): + return x + y + + +def fslg_e12(x, min, max, u, e): + return (fslg_e13(x, min, max, u) / e) * 100 + + +def getGrowthProbability(uN, aList): + # In: uN - user_term, cList - candidate sites, oList - candidate site charges + # Out: list of prob + cList = splitList(aList, 0) + oList = splitList(aList, 1) + Omin, Omax = getLowHigh(oList) + + if Omin == Omax: + Omax += notZero + Omin -= notZero + + PdL = [] + E = notZero + minL = [Omin for q in range(len(oList))] + maxL = [Omax for q in range(len(oList))] + uNL = [uN for q in range(len(oList))] + E = sum(map(fslg_e13, oList, minL, maxL, uNL)) + EL = [E for q in range(len(oList))] + mp = map(fslg_e12, oList, minL, maxL, uNL, EL) + + for m in mp: + PdL.append(m) + + return PdL + + +def updatePointCharges(p, cList, eList=[]): + # In: pNew - new growth cell + # cList - old candidate sites, eList -SAME + # Out: list of new charge at candidate sites + r1 = 1 / 2 # (FSLG - Eqn. 10) + nOiL = [] + for oi in range(len(cList)): + o = cList[oi][1] + c = cList[oi][0] + iOe = 0 + rit = dist(c[0], c[1], c[2], p[0], p[1], p[2]) + iOe += (1 - (r1 / rit)) + Oit = o + iOe + nOiL.append((c, Oit)) + + return nOiL + + +def initialPointCharges(pList, cList, eList=[]): + # In: p -CHARGED CELL (XYZ), cList -candidate sites (XYZ, POT, PROB) + # Out: cList -with potential calculated + r1 = 1 / 2 # (FSLG - Eqn. 10) + npList = [] + for p in pList: + npList.append(((p[0], p[1], p[2]), 1.0)) + for e in eList: + npList.append(((e[0], e[1], e[2]), e[3])) + OiL = [] + for i in cList: + Oi = 0 + for j in npList: + if i != j[0]: + rij = dist(i[0], i[1], i[2], j[0][0], j[0][1], j[0][2]) + Oi += (1 - (r1 / rij)) * j[1] # charge influence + OiL.append(((i[0], i[1], i[2]), Oi)) + + return OiL + + +def getCandidateSites(aList, iList=[]): + # In: aList -(X,Y,Z) of charged cell sites, iList - insulator sites + # Out: candidate list of growth sites [(X,Y,Z)] + cList = [] + for c in aList: + tempList = getStencil3D_26(c[0], c[1], c[2]) + for t in tempList: + if t not in aList and t not in iList: + cList.append(t) + ncList = deDupe(cList) + + return ncList + + +# Setup functions + +def setupObjects(): + winmgr = bpy.context.scene.advanced_objects + oOB = bpy.data.objects.new('ELorigin', None) + oOB.location = ((0, 0, 10)) + bpy.context.scene.objects.link(oOB) + + gOB = bpy.data.objects.new('ELground', None) + gOB.empty_draw_type = 'ARROWS' + bpy.context.scene.objects.link(gOB) + + cME = makeMeshCube(1) + cOB = bpy.data.objects.new('ELcloud', cME) + cOB.location = ((-2, 8, 12)) + cOB.hide_render = True + bpy.context.scene.objects.link(cOB) + + iME = makeMeshCube(1) + for v in iME.vertices: + xyl = 6.5 + zl = .5 + v.co[0] = v.co[0] * xyl + v.co[1] = v.co[1] * xyl + v.co[2] = v.co[2] * zl + iOB = bpy.data.objects.new('ELinsulator', iME) + iOB.location = ((0, 0, 5)) + iOB.hide_render = True + bpy.context.scene.objects.link(iOB) + + try: + winmgr.OOB = 'ELorigin' + winmgr.GOB = 'ELground' + winmgr.COB = 'ELcloud' + winmgr.IOB = 'ELinsulator' + except: + pass + + +def checkSettings(): + check = True + winmgr = bpy.context.scene.advanced_objects + message = "" + if winmgr.OOB == "": + message = "Error: no origin object selected" + check = False + + if winmgr.GROUNDBOOL and winmgr.GOB == "": + message = "Error: no ground object selected" + check = False + + if winmgr.CLOUDBOOL and winmgr.COB == "": + message = "Error: no cloud object selected" + check = False + + if winmgr.IBOOL and winmgr.IOB == "": + message = "Error: no insulator object selected" + check = False + + if check is False: + debug_prints(func="checkSettings", text=message) + + # return state and the message for the operator report + return check, message + + +# Main + +def FSLG(): + winmgr = bpy.context.scene.advanced_objects + # fast simulation of laplacian growth + debug_prints(func="FSLG", + text="Go go gadget: fast simulation of laplacian growth") + tc1 = time.clock() + TSTEPS = winmgr.TSTEPS + + obORIGIN = bpy.context.scene.objects[winmgr.OOB] + obGROUND = bpy.context.scene.objects[winmgr.GOB] + winmgr.ORIGIN = obORIGIN.location + winmgr.GROUNDZ = int((obGROUND.location[2] - winmgr.ORIGIN[2]) / winmgr.GSCALE) + + # 1) insert intial charge(s) point (uses verts if mesh) + cgrid = [(0, 0, 0)] + + if obORIGIN.type == 'MESH': + debug_prints( + func="FSLG", + text="Origin object is mesh, 'voxelizing' intial charges from verts" + ) + cgrid = voxelByVertex(obORIGIN, winmgr.GSCALE) + + if winmgr.VMMESH: + debug_prints( + func="FSLG", + text="Cannot classify stroke from vert origins yet, no multi-mesh output" + ) + winmgr.VMMESH = False + winmgr.VSMESH = True + + # ground charge cell / insulator lists (echargelist/iclist) + eChargeList = [] + icList = [] + if winmgr.GROUNDBOOL: + eChargeList = fakeGroundChargePlane(winmgr.GROUNDZ, winmgr.GROUNDC) + + if winmgr.CLOUDBOOL: + debug_prints( + func="FSLG", + text="'Voxelizing' cloud object (could take some time)" + ) + obCLOUD = bpy.context.scene.objects[winmgr.COB] + eChargeListQ = voxelByRays(obCLOUD, winmgr.ORIGIN, winmgr.GSCALE) + eChargeList = addCharges(eChargeListQ, winmgr.CLOUDC) + debug_prints( + func="FSLG", + text="cloud object cell count", var=len(eChargeList) + ) + + if winmgr.IBOOL: + debug_prints( + func="FSLG", + text="'Voxelizing' insulator object (could take some time)" + ) + obINSULATOR = bpy.context.scene.objects[winmgr.IOB] + icList = voxelByRays(obINSULATOR, winmgr.ORIGIN, winmgr.GSCALE) + + debug_prints( + func="FSLG", + text="Insulator object cell count", var=len(icList) + ) + + # 2) locate candidate sites around charge + cSites = getCandidateSites(cgrid, icList) + + # 3) calc potential at each site (eqn. 10) + cSites = initialPointCharges(cgrid, cSites, eChargeList) + + ts = 1 + while ts <= TSTEPS: + # 1) select new growth site (eqn. 12) + # get probabilities at candidate sites + gProbs = getGrowthProbability(winmgr.BIGVAR, cSites) + # choose new growth site based on probabilities + gSitei = weightedRandomChoice(gProbs) + gsite = cSites[gSitei][0] + + # 2) add new point charge at growth site + # add new growth cell to grid + cgrid.append(gsite) + # remove new growth cell from candidate sites + cSites.remove(cSites[gSitei]) + + # 3) update potential at candidate sites (eqn. 11) + cSites = updatePointCharges(gsite, cSites, eChargeList) + + # 4) add new candidates surrounding growth site + # get candidate 'stencil' + ncSitesT = getCandidateSites([gsite], icList) + # remove candidates already in candidate list or charge grid + ncSites = [] + cSplit = splitList(cSites, 0) + for cn in ncSitesT: + if cn not in cSplit and cn not in cgrid: + ncSites.append((cn, 0)) + + # 5) calc potential at new candidate sites (eqn. 10) + ncSplit = splitList(ncSites, 0) + ncSites = initialPointCharges(cgrid, ncSplit, eChargeList) + + # add new candidate sites to candidate list + for ncs in ncSites: + cSites.append(ncs) + + # iteration complete + istr1 = ':::T-STEP: ' + str(ts) + '/' + str(TSTEPS) + istr12 = ' | GROUNDZ: ' + str(winmgr.GROUNDZ) + ' | ' + istr2 = 'CANDS: ' + str(len(cSites)) + ' | ' + istr3 = 'GSITE: ' + str(gsite) + debug_prints( + func="FSLG", + text="Iteration complete", + var=istr1 + istr12 + istr2 + istr3 + ) + ts += 1 + + # early termination for ground/cloud strike + if winmgr.GROUNDBOOL: + if gsite[2] == winmgr.GROUNDZ: + ts = TSTEPS + 1 + debug_prints( + func="FSLG", + text="Early termination due to groundstrike" + ) + continue + + if winmgr.CLOUDBOOL: + if gsite in splitListCo(eChargeList): + ts = TSTEPS + 1 + debug_prints( + func="FSLG", + text="Early termination due to cloudstrike" + ) + continue + + tc2 = time.clock() + tcRUN = tc2 - tc1 + debug_prints( + func="FSLG", + text="Laplacian growth loop completed", + var=str(len(cgrid)) + " / " + str(tcRUN)[0:5] + " Seconds" + ) + debug_prints(func="FSLG", text="Visualizing data") + + reportSTRING = getReportString(tcRUN) + + # Visualize array + visualizeArray( + cgrid, obORIGIN, winmgr.GSCALE, + winmgr.VMMESH, winmgr.VSMESH, + winmgr.VCUBE, winmgr.VVOX, reportSTRING + ) + + debug_prints(func="FSLG", text="COMPLETE") + + +# GUI # + +class runFSLGLoopOperator(Operator): + bl_idname = "object.runfslg_operator" + bl_label = "run FSLG Loop Operator" + bl_description = "By The Mighty Hammer Of Thor!!!" + + def execute(self, context): + # tuple - state, report text + is_conditions, message = checkSettings() + + if is_conditions: + FSLG() + else: + self.report({'WARNING'}, message + " Operation Cancelled") + + return {'CANCELLED'} + + return {'FINISHED'} + + +class setupObjectsOperator(Operator): + bl_idname = "object.setup_objects_operator" + bl_label = "Setup Objects Operator" + bl_description = "Create origin/ground/cloud/insulator objects" + + def execute(self, context): + setupObjects() + + return {'FINISHED'} + + +class OBJECT_PT_fslg(Panel): + bl_label = "Laplacian Lightning" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_context = "objectmode" + bl_category = "Create" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + winmgr = context.scene.advanced_objects + + col = layout.column(align=True) + col.prop(winmgr, "TSTEPS") + col.prop(winmgr, "GSCALE") + col.prop(winmgr, "BIGVAR") + + col = layout.column() + col.operator("object.setup_objects_operator", text="Create Setup objects") + col.label("Origin object") + col.prop_search(winmgr, "OOB", context.scene, "objects") + + box = layout.box() + col = box.column() + col.prop(winmgr, "GROUNDBOOL") + if winmgr.GROUNDBOOL: + col.prop_search(winmgr, "GOB", context.scene, "objects") + col.prop(winmgr, "GROUNDC") + + box = layout.box() + col = box.column() + col.prop(winmgr, "CLOUDBOOL") + if winmgr.CLOUDBOOL: + col.prop_search(winmgr, "COB", context.scene, "objects") + col.prop(winmgr, "CLOUDC") + + box = layout.box() + col = box.column() + col.prop(winmgr, "IBOOL") + if winmgr.IBOOL: + col.prop_search(winmgr, "IOB", context.scene, "objects") + + col = layout.column() + col.operator("object.runfslg_operator", + text="Generate Lightning", icon="RNDCURVE") + + row = layout.row(align=True) + row.prop(winmgr, "VMMESH", toggle=True) + row.prop(winmgr, "VSMESH", toggle=True) + row.prop(winmgr, "VCUBE", toggle=True) + + +def getReportString(rtime): + winmgr = bpy.context.scene.advanced_objects + rSTRING1 = 't:' + str(winmgr.TSTEPS) + ',sc:' + str(winmgr.GSCALE)[0:4] + ',uv:' + str(winmgr.BIGVAR)[0:4] + ',' + rSTRING2 = 'ori:' + str(winmgr. ORIGIN[0]) + '/' + str(winmgr. ORIGIN[1]) + '/' + str(winmgr. ORIGIN[2]) + ',' + rSTRING3 = 'gz:' + str(winmgr.GROUNDZ) + ',gc:' + str(winmgr.GROUNDC) + ',rtime:' + str(int(rtime)) + return rSTRING1 + rSTRING2 + rSTRING3 + + +def addReportProp(ob, str): + bpy.types.Object.FSLG_REPORT = bpy.props.StringProperty( + name='fslg_report', default='') + ob.FSLG_REPORT = str + + +def register(): + bpy.utils.register_class(runFSLGLoopOperator) + bpy.utils.register_class(setupObjectsOperator) + bpy.utils.register_class(OBJECT_PT_fslg) + + +def unregister(): + bpy.utils.unregister_class(runFSLGLoopOperator) + bpy.utils.unregister_class(setupObjectsOperator) + bpy.utils.unregister_class(OBJECT_PT_fslg) + + +if __name__ == "__main__": + register() + pass + + +# Benchmarks Function + +def BENCH(): + debug_prints(func="BENCH", text="BEGIN BENCHMARK") + bt0 = time.clock() + # make a big list + tsize = 25 + tlist = [] + for x in range(tsize): + for y in range(tsize): + for z in range(tsize): + tlist.append((x, y, z)) + tlist.append((x, y, z)) + + # function to test + bt1 = time.clock() + bt2 = time.clock() + btRUNb = bt2 - bt1 + btRUNa = bt1 - bt0 + + debug_prints(func="BENCH", text="SETUP TIME", var=btRUNa) + debug_prints(func="BENCH", text="BENCHMARK TIME", var=btRUNb) + debug_print_vars( + "\n[BENCH]\n", + "GRIDSIZE: ", tsize, ' - ', tsize * tsize * tsize + ) diff --git a/add_advanced_objects_panels/object_mangle_tools.py b/add_advanced_objects_panels/object_mangle_tools.py new file mode 100644 index 00000000..c36bb224 --- /dev/null +++ b/add_advanced_objects_panels/object_mangle_tools.py @@ -0,0 +1,208 @@ +# mangle_tools.py (c) 2011 Phil Cote (cotejrp1) + +# ###### 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 LICENCE BLOCK ###### + +# Note: properties are moved into __init__ + +bl_info = { + "name": "Mangle Tools", + "author": "Phil Cote", + "blender": (2, 71, 0), + "location": "View3D > Toolshelf > Tools Tab", + "description": "Set of tools to mangle curves, meshes, and shape keys", + "warning": "", + "wiki_url": "", + "category": "Object"} + + +import bpy +import random +from bpy.types import ( + Operator, + Panel, + ) +import time +from math import pi +import bmesh + + +def move_coordinate(context, co, is_curve=False): + advanced_objects = context.scene.advanced_objects + xyz_const = advanced_objects.mangle_constraint_vector + random.seed(time.time()) + multiplier = 1 + + # For curves, we base the multiplier on the circumference formula. + # This helps make curve changes more noticable. + if is_curve: + multiplier = 2 * pi + random_mag = advanced_objects.mangle_random_magnitude + if xyz_const[0]: + co.x += .01 * random.randrange(-random_mag, random_mag) * multiplier + if xyz_const[1]: + co.y += .01 * random.randrange(-random_mag, random_mag) * multiplier + if xyz_const[2]: + co.z += .01 * random.randrange(-random_mag, random_mag) * multiplier + + +class MeshManglerOperator(Operator): + bl_idname = "ba.mesh_mangler" + bl_label = "Mangle Mesh" + bl_description = ("Push vertices on the selected object around in random\n" + "directions to create a crumpled look") + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + ob = context.active_object + return ob is not None and ob.type == 'MESH' + + def execute(self, context): + mesh = context.active_object.data + bm = bmesh.new() + bm.from_mesh(mesh) + verts = bm.verts + advanced_objects = context.scene.advanced_objects + randomMag = advanced_objects.mangle_random_magnitude + random.seed(time.time()) + + if mesh.shape_keys is not None: + self.report({'INFO'}, + "Cannot mangle mesh: Shape keys present. Operation Cancelled") + return {'CANCELLED'} + + for vert in verts: + xVal = .01 * random.randrange(-randomMag, randomMag) + yVal = .01 * random.randrange(-randomMag, randomMag) + zVal = .01 * random.randrange(-randomMag, randomMag) + + vert.co.x = vert.co.x + xVal + vert.co.y = vert.co.y + yVal + vert.co.z = vert.co.z + zVal + + del verts + + bm.to_mesh(mesh) + mesh.update() + + return {'FINISHED'} + + +class AnimanglerOperator(Operator): + bl_idname = "ba.ani_mangler" + bl_label = "Mangle Shape Key" + bl_description = ("Make a shape key and pushes the verts around on it\n" + "to set up for random pulsating animation") + + @classmethod + def poll(cls, context): + ob = context.active_object + return ob is not None and ob.type in ['MESH', 'CURVE'] + + def execute(self, context): + scn = context.scene.advanced_objects + mangleName = scn.mangle_name + ob = context.object + shapeKey = ob.shape_key_add(name=mangleName) + verts = shapeKey.data + + for vert in verts: + move_coordinate(context, vert.co, is_curve=ob.type == 'CURVE') + + return {'FINISHED'} + + +class CurveManglerOp(Operator): + bl_idname = "ba.curve_mangler" + bl_label = "Mangle Curve" + bl_description = "Mangle a curve to the degree the user specifies" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + ob = context.active_object + return ob is not None and ob.type == "CURVE" + + def execute(self, context): + ob = context.active_object + if ob.data.shape_keys is not None: + self.report({'INFO'}, + "Cannot mangle curve. Shape keys present. Operation Cancelled") + return {'CANCELLED'} + + splines = context.object.data.splines + + for spline in splines: + if spline.type == 'BEZIER': + points = spline.bezier_points + elif spline.type in {'POLY', 'NURBS'}: + points = spline.points + + for point in points: + move_coordinate(context, point.co, is_curve=True) + + return {'FINISHED'} + + +class MangleToolsPanel(Panel): + bl_label = "Mangle Tools" + bl_space_type = "VIEW_3D" + bl_context = "objectmode" + bl_region_type = "TOOLS" + bl_category = "Create" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + scn = context.scene.advanced_objects + obj = context.object + + if obj and obj.type in ['MESH']: + layout = self.layout + + row = layout.row(align=True) + row.prop(scn, "mangle_constraint_vector", toggle=True) + + col = layout.column() + col.prop(scn, "mangle_random_magnitude") + col.operator("ba.mesh_mangler") + col.separator() + + col.prop(scn, "mangle_name") + col.operator("ba.ani_mangler") + else: + layout = self.layout + layout.label(text="Please select a Mesh Object", icon="INFO") + + +def register(): + bpy.utils.register_class(AnimanglerOperator) + bpy.utils.register_class(MeshManglerOperator) + bpy.utils.register_class(CurveManglerOp) + bpy.utils.register_class(MangleToolsPanel) + + +def unregister(): + bpy.utils.unregister_class(AnimanglerOperator) + bpy.utils.unregister_class(MeshManglerOperator) + bpy.utils.unregister_class(MangleToolsPanel) + bpy.utils.unregister_class(CurveManglerOp) + + +if __name__ == "__main__": + register() diff --git a/add_advanced_objects_panels/oscurart_constellation.py b/add_advanced_objects_panels/oscurart_constellation.py new file mode 100644 index 00000000..6e840974 --- /dev/null +++ b/add_advanced_objects_panels/oscurart_constellation.py @@ -0,0 +1,141 @@ +# ##### 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 ##### + +bl_info = { + "name": "Mesh: Constellation", + "author": "Oscurart", + "blender": (2, 67, 0), + "location": "Add > Mesh > Constellation", + "description": "Create a new Mesh From Selected", + "warning": "", + "wiki_url": "", + "category": "Add Mesh"} + +# Note the setting is moved to __init__ search for +# the adv_obj and advanced_objects patterns + +import bpy +from bpy.props import FloatProperty +from math import sqrt +from bpy.types import ( + Operator, + Panel, + ) + +def VertDis(a, b): + dst = sqrt(pow(a.co.x - b.co.x, 2) + + pow(a.co.y - b.co.y, 2) + + pow(a.co.z - b.co.z, 2)) + return(dst) + + +def OscConstellation(limit): + actobj = bpy.context.object + vertlist = [] + edgelist = [] + edgei = 0 + + for ind, verta in enumerate(actobj.data.vertices[:]): + for vertb in actobj.data.vertices[ind:]: + if VertDis(verta, vertb) <= limit: + vertlist.append(verta.co[:]) + vertlist.append(vertb.co[:]) + edgelist.append((edgei, edgei + 1)) + edgei += 2 + + mesh = bpy.data.meshes.new("rsdata") + obj = bpy.data.objects.new("rsObject", mesh) + bpy.context.scene.objects.link(obj) + mesh.from_pydata(vertlist, edgelist, []) + + +class Oscurart_Constellation(Operator): + bl_idname = "mesh.constellation" + bl_label = "Constellation" + bl_description = ("Create a Constellation Mesh - Cloud of Vertices\n" + "Note: can produce a lot of geometry\n" + "Needs an existing Active Mesh Object") + bl_options = {'REGISTER', 'UNDO'} + + limit = FloatProperty( + name="Threshold", + description="Edges will be created only if the distance\n" + "between vertices is smaller than this value", + default=2, + min=0 + ) + + @classmethod + def poll(cls, context): + obj = context.active_object + return (obj and obj.type == "MESH") + + def invoke(self, context, event): + adv_obj = context.scene.advanced_objects + self.limit = adv_obj.constellation_limit + + return self.execute(context) + + def draw(self, context): + layout = self.layout + + layout.prop(self, "limit") + + def execute(self, context): + try: + OscConstellation(self.limit) + except Exception as e: + print("\n[Add Advanced Objects]\nOperator: mesh.constellation\n{}".format(e)) + + self.report({"WARNING"}, + "Constellation Operation could not be Completed (See Console for more Info)") + + return {"CANCELLED"} + + return {'FINISHED'} + +class Constellation_Operator_Panel(Panel): + bl_label = "Constellation" + bl_region_type = "TOOLS" + bl_space_type = "VIEW_3D" + bl_options = {'DEFAULT_CLOSED'} + bl_context = "objectmode" + bl_category = "Create" + + def draw(self, context): + layout = self.layout + adv_obj = context.scene.advanced_objects + + box = layout.box() + col = box.column(align=True) + col.label("Constellation:") + col.operator("mesh.constellation", text="Cross Section") + col.prop(adv_obj, "constellation_limit") +# Register + +def register(): + bpy.utils.register_class(Oscurart_Constellation) + bpy.utils.register_class(Constellation_Operator_Panel) + + +def unregister(): + bpy.utils.unregister_class(Oscurart_Constellation) + bpy.utils.unregister_class(Constellation_Operator_Panel) + +if __name__ == "__main__": + register() diff --git a/add_advanced_objects_panels/unfold_transition.py b/add_advanced_objects_panels/unfold_transition.py new file mode 100644 index 00000000..60e612dd --- /dev/null +++ b/add_advanced_objects_panels/unfold_transition.py @@ -0,0 +1,346 @@ +# gpl: authors Liero, Atom + +bl_info = { + "name": "Unfold transition", + "author": "Liero, Atom", + "location": "Tool bar > Animation tab > UnFold Transition", + "description": "Simple unfold transition / animation, will " + "separate faces and set up an armature", + "category": "Animation"} + +# Note the properties are moved to __init__ +# search for patterns advanced_objects, adv_obj + +import bpy +from bpy.types import ( + Operator, + Panel, + ) +from random import ( + randint, + uniform, + ) +from mathutils import Vector +from mathutils.geometry import intersect_point_line + + +class Set_Up_Fold(Operator): + bl_idname = "object.set_up_fold" + bl_label = "Set Up Unfold" + bl_description = ("Set up Faces and Bones for animation\n" + "Needs an existing Active Mesh Object") + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + obj = context.active_object + return (obj is not None and obj.type == "MESH") + + def execute(self, context): + bpy.ops.object.mode_set() + scn = bpy.context.scene + adv_obj = scn.advanced_objects + obj = bpy.context.object + dat = obj.data + fac = dat.polygons + ver = dat.vertices + + # try to cleanup traces of previous actions + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.remove_doubles(threshold=0.0001, use_unselected=True) + bpy.ops.object.mode_set() + old_vg = [vg for vg in obj.vertex_groups if vg.name.startswith("bone.")] + for vg in old_vg: + obj.vertex_groups.remove(vg) + + if "UnFold" in obj.modifiers: + arm = obj.modifiers["UnFold"].object + rig = arm.data + try: + scn.objects.unlink(arm) + bpy.data.objects.remove(arm) + bpy.data.armatures.remove(rig) + except: + pass + obj.modifiers.remove(obj.modifiers["UnFold"]) + + # try to obtain the face sequence from the vertex weights + if adv_obj.unfold_modo == "weight": + if len(obj.vertex_groups): + i = obj.vertex_groups.active.index + W = [] + for f in fac: + v_data = [] + for v in f.vertices: + try: + w = ver[v].groups[i].weight + v_data.append((w, v)) + except: + v_data.append((0, v)) + v_data.sort(reverse=True) + v1 = ver[v_data[0][1]].co + v2 = ver[v_data[1][1]].co + cen = Vector(f.center) + its = intersect_point_line(cen, v2, v1) + head = v2.lerp(v1, its[1]) + peso = sum([x[0] for x in v_data]) + W.append((peso, f.index, cen, head)) + W.sort(reverse=True) + S = [x[1:] for x in W] + else: + self.report({"INFO"}, "First paint a Weight Map for this object") + + return {"FINISHED"} + + # separate the faces and sort them + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.select_all(action="SELECT") + bpy.ops.mesh.edge_split() + bpy.ops.mesh.select_all(action="SELECT") + + if adv_obj.unfold_modo == "cursor": + bpy.context.tool_settings.mesh_select_mode = [True, True, True] + bpy.ops.mesh.sort_elements( + type="CURSOR_DISTANCE", elements={"VERT", "EDGE", "FACE"} + ) + bpy.context.tool_settings.mesh_select_mode = [False, False, True] + bpy.ops.object.mode_set() + + # Get sequence of faces and edges from the face / vertex indices + if adv_obj.unfold_modo != "weight": + S = [] + for f in fac: + E = list(f.edge_keys) + E.sort() + v1 = ver[E[0][0]].co + v2 = ver[E[0][1]].co + cen = Vector(f.center) + its = intersect_point_line(cen, v2, v1) + head = v2.lerp(v1, its[1]) + S.append((f.index, f.center, head)) + + # create the armature and the modifier + arm = bpy.data.armatures.new("arm") + rig = bpy.data.objects.new("rig_" + obj.name, arm) + + # store the name for checking the right rig + adv_obj.unfold_arm_name = rig.name + rig.matrix_world = obj.matrix_world + scn.objects.link(rig) + scn.objects.active = rig + bpy.ops.object.mode_set(mode="EDIT") + arm.draw_type = "WIRE" + rig.show_x_ray = True + mod = obj.modifiers.new("UnFold", "ARMATURE") + mod.show_in_editmode = True + mod.object = rig + + # create bones and vertex groups + root = arm.edit_bones.new("bone.000") + root.tail = (0, 0, 0) + root.head = (0, 0, 1) + root.select = True + vis = [False, True] + [False] * 30 + + for fb in S: + f = fac[fb[0]] + b = arm.edit_bones.new("bone.000") + if adv_obj.unfold_flip: + b.tail, b.head = fb[2], fb[1] + else: + b.tail, b.head = fb[1], fb[2] + + b.align_roll(f.normal) + b.select = False + b.layers = vis + b.parent = root + vg = obj.vertex_groups.new(b.name) + vg.add(f.vertices, 1, "ADD") + + bpy.ops.object.mode_set() + + if adv_obj.unfold_modo == "weight": + obj.vertex_groups.active_index = 0 + scn.objects.active = rig + obj.select = False + + return {"FINISHED"} + + +class Animate_Fold(Operator): + bl_idname = "object.animate_fold" + bl_label = "Animate Unfold" + bl_description = ("Animate bones to simulate unfold. Starts on current frame\n" + "Needs an existing Active Armature Object created in the previous step") + bl_options = {"REGISTER", "UNDO"} + + is_not_undo = False + + @classmethod + def poll(cls, context): + obj = context.active_object + return (obj is not None and obj.type == "ARMATURE" and obj.is_visible(bpy.context.scene)) + + def draw(self, context): + layout = self.layout + adv_obj = context.scene.advanced_objects + + if self.is_not_undo is True: + layout.label(text="Warning:", icon="INFO") + layout.label(text="The generated Armature was not selected or it was renamed") + layout.label(text="The animation can fail if it is not generated by the previous step") + layout.separator() + layout.label(text="Expected Armature name:", icon="BONE_DATA") + layout.label(text=str(adv_obj.unfold_arm_name), icon="TRIA_RIGHT") + layout.label(text="To Continue press OK, to Cancel click Outside the Pop-up") + layout.separator() + else: + return + + def invoke(self, context, event): + obj = bpy.context.object + scn = bpy.context.scene + adv_obj = scn.advanced_objects + + if obj.name != adv_obj.unfold_arm_name: + self.is_not_undo = True + return context.window_manager.invoke_props_dialog(self, width=400) + else: + return self.execute(context) + + def execute(self, context): + obj = bpy.context.object + scn = bpy.context.scene + adv_obj = scn.advanced_objects + fra = scn.frame_current + if obj.name != adv_obj.unfold_arm_name: + self.report({"INFO"}, + "The generated rig was not selected or renamed. The animation can fail") + # clear the animation and get the list of bones + if obj.animation_data: + obj.animation_data_clear() + bpy.ops.object.mode_set(mode="POSE") + bones = obj.pose.bones[0].children_recursive + + if adv_obj.unfold_flip: + rot = -3.141592 + else: + rot = adv_obj.unfold_rot_max / 57.3 + + extra = adv_obj.unfold_rot_time * adv_obj.unfold_bounce + ruido = max(adv_obj.unfold_rot_time + extra, + adv_obj.unfold_sca_time) + adv_obj.unfold_fold_noise + + len_bones = len(bones) if len(bones) != 0 else 1 # possible division by zero + vel = (adv_obj.unfold_fold_duration - ruido) / len_bones + + # introduce scale and rotation keyframes + for a, b in enumerate(bones): + t = fra + a * vel + randint(0, adv_obj.unfold_fold_noise) + + if adv_obj.unfold_flip: + b.scale = (1, 1, 1) + elif adv_obj.unfold_from_point: + b.scale = (0, 0, 0) + else: + b.scale = (1, 0, 0) + + if not adv_obj.unfold_flip: + b.keyframe_insert("scale", frame=t) + b.scale = (1, 1, 1) + b.keyframe_insert("scale", frame=t + adv_obj.unfold_sca_time) + + if adv_obj.unfold_rot_max: + b.rotation_mode = "XYZ" + if adv_obj.unfold_wiggle_rot: + euler = (uniform(-rot, rot), uniform(-rot, rot), uniform(-rot, rot)) + else: + euler = (rot, 0, 0) + + b.rotation_euler = euler + b.keyframe_insert("rotation_euler", frame=t) + + if adv_obj.unfold_bounce: + val = adv_obj.unfold_bounce * -.10 + b.rotation_euler = (val * euler[0], val * euler[1], val * euler[2]) + b.keyframe_insert( + "rotation_euler", frame=t + adv_obj.unfold_rot_time + .25 * extra + ) + + val = adv_obj.unfold_bounce * .05 + b.rotation_euler = (val * euler[0], val * euler[1], val * euler[2]) + b.keyframe_insert( + "rotation_euler", frame=t + adv_obj.unfold_rot_time + .50 * extra + ) + + val = adv_obj.unfold_bounce * -.025 + b.rotation_euler = (val * euler[0], val * euler[1], val * euler[2]) + b.keyframe_insert( + "rotation_euler", frame=t + adv_obj.unfold_rot_time + .75 * extra + ) + + b.rotation_euler = (0, 0, 0) + b.keyframe_insert( + "rotation_euler", frame=t + adv_obj.unfold_rot_time + extra + ) + self.is_not_undo = False + + return {"FINISHED"} + + +class PanelFOLD(Panel): + bl_label = "Unfold Transition" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Create" + bl_context = "objectmode" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + layout = self.layout + adv_obj = context.scene.advanced_objects + + box = layout.box() + col = box.column() + col.operator("object.set_up_fold", text="1. Set Up Unfold") + col.separator() + col.label("Unfold Mode:") + col.prop(adv_obj, "unfold_modo") + col.prop(adv_obj, "unfold_flip") + + box = layout.box() + col = box.column(align=True) + col.operator("object.animate_fold", text="2. Animate Unfold") + col.separator() + col.prop(adv_obj, "unfold_fold_duration") + col.prop(adv_obj, "unfold_sca_time") + col.prop(adv_obj, "unfold_rot_time") + col.prop(adv_obj, "unfold_rot_max") + + row = col.row(align=True) + row.prop(adv_obj, "unfold_fold_noise") + row.prop(adv_obj, "unfold_bounce") + row = col.row(align=True) + row.prop(adv_obj, "unfold_wiggle_rot") + + if not adv_obj.unfold_flip: + row.prop(adv_obj, "unfold_from_point") + +classes = ( + Set_Up_Fold, + Animate_Fold, + PanelFOLD, + ) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) + + +if __name__ == "__main__": + register() |