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

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'mesh_inset/model.py')
-rw-r--r--mesh_inset/model.py575
1 files changed, 575 insertions, 0 deletions
diff --git a/mesh_inset/model.py b/mesh_inset/model.py
new file mode 100644
index 00000000..a3eb2aac
--- /dev/null
+++ b/mesh_inset/model.py
@@ -0,0 +1,575 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <pep8 compliant>
+
+"""Manipulations of Models.
+"""
+
+__author__ = "howard.trickey@gmail.com"
+
+from . import geom
+from . import triquad
+from . import offset
+import math
+
+
+def PolyAreasToModel(polyareas, bevel_amount, bevel_pitch, quadrangulate):
+ """Convert a PolyAreas into a Model object.
+
+ Assumes polyareas are in xy plane.
+
+ Args:
+ polyareas: geom.PolyAreas
+ bevel_amount: float - if > 0, amount of bevel
+ bevel_pitch: float - if > 0, angle in radians of bevel
+ quadrangulate: bool - should n-gons be quadrangulated?
+ Returns:
+ geom.Model
+ """
+
+ m = geom.Model()
+ if not polyareas:
+ return m
+ polyareas.points.AddZCoord(0.0)
+ m.points = polyareas.points
+ for pa in polyareas.polyareas:
+ PolyAreaToModel(m, pa, bevel_amount, bevel_pitch, quadrangulate)
+ return m
+
+
+def PolyAreaToModel(m, pa, bevel_amount, bevel_pitch, quadrangulate):
+ if bevel_amount > 0.0:
+ BevelPolyAreaInModel(m, pa, bevel_amount, bevel_pitch, quadrangulate,
+ False)
+ elif quadrangulate:
+ if len(pa.poly) == 0:
+ return
+ qpa = triquad.QuadrangulateFaceWithHoles(pa.poly, pa.holes, pa.points)
+ m.faces.extend(qpa)
+ m.face_data.extend([pa.data] * len(qpa))
+ else:
+ m.faces.append(pa.poly)
+ # TODO: just the first part of QuadrangulateFaceWithHoles, to join
+ # holes to outer poly
+ m.face_data.append(pa.data)
+
+
+def ExtrudePolyAreasInModel(mdl, polyareas, depth, cap_back):
+ """Extrude the boundaries given by polyareas by -depth in z.
+
+ Assumes polyareas are in xy plane.
+
+ Arguments:
+ mdl: geom.Model - where to do extrusion
+ polyareas: geom.Polyareas
+ depth: float
+ cap_back: bool - if True, cap off the back
+ Side Effects:
+ For all edges in polys in polyareas, make quads in Model
+ extending those edges by depth in the negative z direction.
+ The application data will be the data of the face that the edge
+ is part of.
+ """
+
+ for pa in polyareas.polyareas:
+ back_poly = _ExtrudePoly(mdl, pa.poly, depth, pa.data, True)
+ back_holes = []
+ for p in pa.holes:
+ back_holes.append(_ExtrudePoly(mdl, p, depth, pa.data, False))
+ if cap_back:
+ qpa = triquad.QuadrangulateFaceWithHoles(back_poly, back_holes,
+ polyareas.points)
+ # need to reverse each poly to get normals pointing down
+ for i, p in enumerate(qpa):
+ t = list(p)
+ t.reverse()
+ qpa[i] = tuple(t)
+ mdl.faces.extend(qpa)
+ mdl.face_data.extend([pa.data] * len(qpa))
+
+
+def _ExtrudePoly(mdl, poly, depth, data, isccw):
+ """Extrude the poly by -depth in z
+
+ Arguments:
+ mdl: geom.Model - where to do extrusion
+ poly: list of vertex indices
+ depth: float
+ data: application data
+ isccw: True if counter-clockwise
+ Side Effects
+ For all edges in poly, make quads in Model
+ extending those edges by depth in the negative z direction.
+ The application data will be the data of the face that the edge
+ is part of.
+ Returns:
+ list of int - vertices for extruded poly
+ """
+
+ if len(poly) < 2:
+ return
+ extruded_poly = []
+ points = mdl.points
+ if isccw:
+ incr = 1
+ else:
+ incr = -1
+ for i, v in enumerate(poly):
+ vnext = poly[(i + incr) % len(poly)]
+ (x0, y0, z0) = points.pos[v]
+ (x1, y1, z1) = points.pos[vnext]
+ vextrude = points.AddPoint((x0, y0, z0 - depth))
+ vnextextrude = points.AddPoint((x1, y1, z1 - depth))
+ if isccw:
+ sideface = [v, vextrude, vnextextrude, vnext]
+ else:
+ sideface = [v, vnext, vnextextrude, vextrude]
+ mdl.faces.append(sideface)
+ mdl.face_data.append(data)
+ extruded_poly.append(vextrude)
+ return extruded_poly
+
+
+def BevelPolyAreaInModel(mdl, polyarea,
+ bevel_amount, bevel_pitch, quadrangulate, as_percent):
+ """Bevel the interior of polyarea in model.
+
+ This does smart beveling: advancing edges are merged
+ rather than doing an 'overlap'. Advancing edges that
+ hit an opposite edge result in a split into two beveled areas.
+
+ If the polyarea is not in the xy plane, do the work in a
+ transformed model, and then transfer the changes back.
+
+ Arguments:
+ mdl: geom.Model - where to do bevel
+ polyarea geom.PolyArea - area to bevel into
+ bevel_amount: float - if > 0, amount of bevel
+ bevel_pitch: float - if > 0, angle in radians of bevel
+ quadrangulate: bool - should n-gons be quadrangulated?
+ as_percent: bool - if True, interpret amount as percent of max
+ Side Effects:
+ Faces and points are added to model to model the
+ bevel and the interior of the polyareas.
+ """
+
+ pa_norm = polyarea.Normal()
+ if pa_norm == (0.0, 0.0, 1.0):
+ m = mdl
+ pa_rot = polyarea
+ else:
+ (pa_rot, inv_rot, inv_map) = _RotatedPolyAreaToXY(polyarea, pa_norm)
+ # don't have to add the original faces into model, just their points.
+ m = geom.Model()
+ m.points = pa_rot.points
+ vspeed = math.tan(bevel_pitch)
+ off = offset.Offset(pa_rot, 0.0, vspeed)
+ if as_percent:
+ bevel_amount = bevel_amount * off.MaxAmount() / 100.0
+ off.Build(bevel_amount)
+ inner_pas = AddOffsetFacesToModel(m, off, polyarea.data)
+ for pa in inner_pas.polyareas:
+ if quadrangulate:
+ if len(pa.poly) == 0:
+ continue
+ qpa = triquad.QuadrangulateFaceWithHoles(pa.poly, pa.holes,
+ pa.points)
+ m.faces.extend(qpa)
+ m.face_data.extend([pa.data] * len(qpa))
+ else:
+ m.faces.append(pa.poly)
+ m.face_data.append(pa.data)
+ if m != mdl:
+ _AddTransformedPolysToModel(mdl, m.faces, m.points, m.face_data,
+ inv_rot, inv_map)
+
+
+def AddOffsetFacesToModel(mdl, off, data=None):
+ """Add the faces due to an offset into model.
+
+ Returns the remaining interiors of the offset as a PolyAreas.
+
+ Args:
+ mdl: geom.Model - model to add offset faces into
+ off: offset.Offset
+ data: any - application data to be copied to the faces
+ Returns:
+ geom.PolyAreas
+ """
+
+ mdl.points = off.polyarea.points
+ assert(len(mdl.points.pos) == 0 or len(mdl.points.pos[0]) == 3)
+ o = off
+ ostack = []
+ while o:
+ if o.endtime != 0.0:
+ for face in o.facespokes:
+ n = len(face)
+ for i, spoke in enumerate(face):
+ nextspoke = face[(i + 1) % n]
+ v0 = spoke.origin
+ v1 = nextspoke.origin
+ v2 = nextspoke.dest
+ v3 = spoke.dest
+ if v2 == v3:
+ mface = [v0, v1, v2]
+ else:
+ mface = [v0, v1, v2, v3]
+ mdl.faces.append(mface)
+ mdl.face_data.append(data)
+ ostack.extend(o.inneroffsets)
+ if ostack:
+ o = ostack.pop()
+ else:
+ o = None
+ return off.InnerPolyAreas()
+
+
+def BevelSelectionInModel(mdl, bevel_amount, bevel_pitch, quadrangulate,
+ as_region, as_percent):
+ """Bevel all the faces in the model, perhaps as one region.
+
+ If as_region is False, each face is beveled individually,
+ otherwise regions of contiguous faces are merged into
+ PolyAreas and beveled as a whole.
+
+ TODO: something if extracted PolyAreas are not approximately
+ planar.
+
+ Args:
+ mdl: geom.Model
+ bevel_amount: float - amount to inset
+ bevel_pitch: float - angle of bevel side
+ quadrangulate: bool - should insides be quadrangulated?
+ as_region: bool - should faces be merged into regions?
+ as_percent: bool - should amount be interpreted as a percent
+ of the maximum amount (if True) or an absolute amount?
+ Side effect:
+ Beveling faces will be added to the model
+ """
+
+ pas = []
+ if as_region:
+ pas = RegionToPolyAreas(mdl.faces, mdl.points, mdl.face_data)
+ else:
+ for f, face in enumerate(mdl.faces):
+ pas.append(geom.PolyArea(mdl.points, face, [],
+ mdl.face_data[f]))
+ for pa in pas:
+ BevelPolyAreaInModel(mdl, pa,
+ bevel_amount, bevel_pitch, quadrangulate, as_percent)
+
+
+def RegionToPolyAreas(faces, points, data):
+ """Find polygonal outlines induced by union of faces.
+
+ Finds the polygons formed by boundary edges (those not
+ sharing an edge with another face in region_faces), and
+ turns those into PolyAreas.
+ In the general case, there will be holes inside.
+ We want to associate data with the region PolyAreas.
+ Just choose a representative element of data[] when
+ more than one face is combined into a PolyArea.
+
+ Args:
+ faces: list of list of int - each sublist is a face (indices into points)
+ points: geom.Points - gives coordinates for vertices
+ data: list of any - parallel to faces, app data to put in PolyAreas
+ Returns:
+ list of geom.PolyArea
+ """
+
+ ans = []
+ (edges, vtoe) = _GetEdgeData(faces)
+ (face_adj, is_interior_edge) = _GetFaceGraph(faces, edges, vtoe, points)
+ (components, ftoc) = _FindFaceGraphComponents(faces, face_adj)
+ for c in range(len(components)):
+ boundary_edges = set()
+ betodata = dict()
+ vstobe = dict()
+ for e, ((vs, ve), f) in enumerate(edges):
+ if ftoc[f] != c or is_interior_edge[e]:
+ continue
+ boundary_edges.add(e)
+ # vstobe[v] is set of edges leaving v
+ # (could be more than one if boundary touches itself at a vertex)
+ if vs in vstobe:
+ vstobe[vs].append(e)
+ else:
+ vstobe[vs] = [e]
+ betodata[(vs, ve)] = data[f]
+ polys = []
+ poly_data = []
+ while boundary_edges:
+ e = boundary_edges.pop()
+ ((vstart, ve), face_i) = edges[e]
+ poly = [vstart, ve]
+ datum = betodata[(vstart, ve)]
+ while ve != vstart:
+ if ve not in vstobe:
+ print("whoops, couldn't close boundary")
+ break
+ nextes = vstobe[ve]
+ if len(nextes) == 1:
+ nexte = nextes[0]
+ else:
+ # find a next edge with face index face_i
+ # TODO: this is not guaranteed to work,
+ # as continuation edge may have been for a different
+ # face that is now combined with face_i by erasing
+ # interior edges. Find a better algorithm here.
+ nexte = -1
+ for ne_cand in nextes:
+ if edges[ne_cand][1] == face_i:
+ nexte = ne_cand
+ break
+ if nexte == -1:
+ # case mentioned in TODO may have happened;
+ # just choose any nexte - may mess things up
+ nexte = nextes[0]
+ ((_, ve), face_i) = edges[nexte]
+ if nexte not in boundary_edges:
+ print("whoops, nexte not a boundary edge", nexte)
+ break
+ boundary_edges.remove(nexte)
+ if ve != vstart:
+ poly.append(ve)
+ polys.append(poly)
+ poly_data.append(datum)
+ if len(polys) == 0:
+ # can happen if an entire closed polytope is given
+ # at least until we do an edge check
+ return []
+ elif len(polys) == 1:
+ ans.append(geom.PolyArea(points, polys[0], [], poly_data[0]))
+ else:
+ outerf = _FindOuterPoly(polys, points, faces)
+ pa = geom.PolyArea(points, polys[outerf], [], poly_data[outerf])
+ pa.holes = [polys[i] for i in range(len(polys)) if i != outerf]
+ ans.append(pa)
+ return ans
+
+
+def _GetEdgeData(faces):
+ """Find edges from faces, and some lookup dictionaries.
+
+ Args:
+ faces: list of list of int - each a closed CCW polygon of vertex indices
+ Returns:
+ (list of ((int, int), int), dict{ int->list of int}) -
+ list elements are ((startv, endv), face index)
+ dict maps vertices to edge indices
+ """
+
+ edges = []
+ vtoe = dict()
+ for findex, f in enumerate(faces):
+ nf = len(f)
+ for i, v in enumerate(f):
+ endv = f[(i + 1) % nf]
+ edges.append(((v, endv), findex))
+ eindex = len(edges) - 1
+ if v in vtoe:
+ vtoe[v].append(eindex)
+ else:
+ vtoe[v] = [eindex]
+ return (edges, vtoe)
+
+
+def _GetFaceGraph(faces, edges, vtoe, points):
+ """Find the face adjacency graph.
+
+ Faces are adjacent if they share an edge,
+ and the shared edge goes in the reverse direction,
+ and if the angle between them isn't too large.
+
+ Args:
+ faces: list of list of int
+ edges: list of ((int, int), int) - see _GetEdgeData
+ vtoe: dict{ int->list of int } - see _GetEdgeData
+ points: geom.Points
+ Returns:
+ (list of list of int, list of bool) -
+ first list: each sublist is adjacent face indices for each face
+ second list: maps edge index to True if it separates adjacent faces
+ """
+
+ face_adj = [[] for i in range(len(faces))]
+ is_interior_edge = [False] * len(edges)
+ for e, ((vs, ve), f) in enumerate(edges):
+ for othere in vtoe[ve]:
+ ((_, we), g) = edges[othere]
+ if we == vs:
+ # face g is adjacent to face f
+ # TODO: angle check
+ if g not in face_adj[f]:
+ face_adj[f].append(g)
+ is_interior_edge[e] = True
+ # Don't bother with mirror relations, will catch later
+ return (face_adj, is_interior_edge)
+
+
+def _FindFaceGraphComponents(faces, face_adj):
+ """Partition faces into connected components.
+
+ Args:
+ faces: list of list of int
+ face_adj: list of list of int - see _GetFaceGraph
+ Returns:
+ (list of list of int, list of int) -
+ first list partitions face indices into separate lists,
+ each a component
+ second list maps face indices into their component index
+ """
+
+ if not faces:
+ return ([], [])
+ components = []
+ ftoc = [-1] * len(faces)
+ for i in range(len(faces)):
+ if ftoc[i] == -1:
+ compi = len(components)
+ comp = []
+ _FFGCSearch(i, faces, face_adj, ftoc, compi, comp)
+ components.append(comp)
+ return (components, ftoc)
+
+
+def _FFGCSearch(findex, faces, face_adj, ftoc, compi, comp):
+ """Depth first search helper function for _FindFaceGraphComponents
+
+ Searches recursively through all faces connected to findex, adding
+ each face found to comp and setting ftoc for that face to compi.
+ """
+
+ comp.append(findex)
+ ftoc[findex] = compi
+ for otherf in face_adj[findex]:
+ if ftoc[otherf] == -1:
+ _FFGCSearch(otherf, faces, face_adj, ftoc, compi, comp)
+
+
+def _FindOuterPoly(polys, points, faces):
+ """Assuming polys has one CCW-oriented face when looking
+ down average normal of faces, return that one.
+
+ Only one of the faces should have a normal whose dot product
+ with the average normal of faces is positive.
+
+ Args:
+ polys: list of list of int - list of polys given by vertex indices
+ points: geom.Points
+ faces: list of list of int - original selected region, used to find
+ average normal
+ Returns:
+ int - the index in polys of the outermost one
+ """
+
+ if len(polys) < 2:
+ return 0
+ fnorm = (0.0, 0.0, 0.0)
+ for face in faces:
+ if len(face) > 2:
+ fnorm = geom.VecAdd(fnorm, geom.Newell(face, points))
+ if fnorm == (0.0, 0.0, 0.0):
+ return 0
+ # fnorm is really a multiple of the normal, but fine for test below
+ for i, poly in enumerate(polys):
+ if len(poly) > 2:
+ pnorm = geom.Newell(poly, points)
+ if geom.VecDot(fnorm, pnorm) > 0:
+ return i
+ print("whoops, couldn't find an outermost poly")
+ return 0
+
+
+def _RotatedPolyAreaToXY(polyarea, norm):
+ """Return a PolyArea rotated to xy plane.
+
+ Only the points in polyarea will be transferred.
+
+ Args:
+ polyarea: geom.PolyArea
+ norm: the normal for polyarea
+ Returns:
+ (geom.PolyArea, (float, ..., float), dict{ int -> int }) - new PolyArea,
+ 4x3 inverse transform, dict mapping new verts to old ones
+ """
+
+ # find rotation matrix that takes norm to (0,0,1)
+ (nx, ny, nz) = norm
+ if abs(nx) < abs(ny) and abs(nx) < abs(nz):
+ v = (vx, vy, vz) = geom.Norm3(0.0, nz, - ny)
+ elif abs(ny) < abs(nz):
+ v = (vx, vy, vz) = geom.Norm3(nz, 0.0, - nx)
+ else:
+ v = (vx, vy, vz) = geom.Norm3(ny, - nx, 0.0)
+ (ux, uy, uz) = geom.Cross3(v, norm)
+ rotmat = [ux, vx, nx, uy, vy, ny, uz, vz, nz, 0.0, 0.0, 0.0]
+ # rotation matrices are orthogonal, so inverse is transpose
+ invrotmat = [ux, uy, uz, vx, vy, vz, nx, ny, nz, 0.0, 0.0, 0.0]
+ pointmap = dict()
+ invpointmap = dict()
+ newpoints = geom.Points()
+ for poly in [polyarea.poly] + polyarea.holes:
+ for v in poly:
+ vcoords = polyarea.points.pos[v]
+ newvcoords = geom.MulPoint3(vcoords, rotmat)
+ newv = newpoints.AddPoint(newvcoords)
+ pointmap[v] = newv
+ invpointmap[newv] = v
+ pa = geom.PolyArea(newpoints)
+ pa.poly = [pointmap[v] for v in polyarea.poly]
+ pa.holes = [[pointmap[v] for v in hole] for hole in polyarea.holes]
+ pa.data = polyarea.data
+ return (pa, invrotmat, invpointmap)
+
+
+def _AddTransformedPolysToModel(mdl, polys, points, poly_data,
+ transform, pointmap):
+ """Add (transformed) the points and faces to a model.
+
+ Add polys to mdl. The polys have coordinates given by indices
+ into points.pos; those need to be transformed by multiplying by
+ the transform matrix.
+ The vertices may already exist in mdl. Rather than relying on
+ AddPoint to detect the duplicate (transform rounding error makes
+ that dicey), the pointmap dictionar is used to map vertex indices
+ in polys into those in mdl - if they exist already.
+
+ Args:
+ mdl: geom.Model - where to put new vertices, faces
+ polys: list of list of int - each sublist a poly
+ points: geom.Points - coords for vertices in polys
+ poly_data: list of any - parallel to polys
+ transform: (float, ..., float) - 12-tuple, a 4x3 transform matrix
+ pointmap: dict { int -> int } - maps new vertex indices to old ones
+ Side Effects:
+ The model gets new faces and vertices, based on those in polys.
+ We are allowed to modify pointmap, as it will be discarded after call.
+ """
+
+ for i, coords in enumerate(points.pos):
+ if i not in pointmap:
+ p = geom.MulPoint3(coords, transform)
+ pointmap[i] = mdl.points.AddPoint(p)
+ for i, poly in enumerate(polys):
+ mpoly = [pointmap[v] for v in poly]
+ mdl.faces.append(mpoly)
+ mdl.face_data.append(poly_data[i])