diff --git a/README.md b/README.md index 43c7d48..9797179 100644 --- a/README.md +++ b/README.md @@ -49,17 +49,10 @@ ## Abstractions -- Encode the notions of "generator which transforms an - existing list of boundaries", "generator which transforms - another generator" - This has a lot of functions parametrized over a lot of functions. Need to work with this somehow. (e.g. should it subdivide this boundary? should it merge opening/closing boundaries?) -- Work directly with lists of boundaries. The only thing - I ever do with them is apply transforms to all of them, or - join adjacent ones with corresponding elements. - - Some generators produce boundaries that can be directly merged and produce sensible geometry. Some generators produce boundaries that are only usable when they are further @@ -70,6 +63,9 @@ they are all scaled in the correct way (some linearly, others inversely perhaps), generated geometry that is more or less identical except that it is higher-resolution? +- Use mixins to extend 3D transformations to things (matrices, + cages, meshes, existing transformations) + ## ???? - Embed this in Blender? diff --git a/cage.py b/cage.py new file mode 100644 index 0000000..946c469 --- /dev/null +++ b/cage.py @@ -0,0 +1,123 @@ +import itertools + +import meshutil +import stl.mesh +import numpy + +class Cage(object): + """An ordered list of polygons (or polytopes, technically).""" + def __init__(self, verts, splits): + # Element i of 'self.splits' gives the row index in 'self.verts' + # in which polygon i begins. + self.splits = splits + # NumPy array of shape (N,3) + self.verts = verts + @classmethod + def from_arrays(cls, *arrs): + """ + Pass any number of array-like objects, with each one being a + nested array with 3 elements - e.g. [[0,0,0], [1,1,1], [2,2,2]] - + providing points. + Each array-like object is treated as vertices describing a + polygon/polytope. + """ + n = 0 + splits = [0]*len(arrs) + for i,arr in enumerate(arrs): + splits[i] = n + n += len(arr) + verts = numpy.zeros((n,3), dtype=numpy.float64) + # Populate it accordingly: + i0 = 0 + for arr in arrs: + i1 = i0 + len(arr) + verts[i0:i1, :] = arr + i0 = i1 + return cls(verts, splits) + def polys(self): + """Return iterable of polygons as (views of) NumPy arrays.""" + count = len(self.splits) + for i,n0 in enumerate(self.splits): + if i+1 < count: + n1 = self.splits[i+1] + yield self.verts[n0:n1,:] + else: + yield self.verts[n0:,:] + + def is_fork(self): + return False + def transform(self, xform): + """Apply a Transform to all vertices, returning a new Cage.""" + return Cage(xform.apply_to(self.verts), self.splits) + +class CageFork(object): + """A series of generators that all split off in such a way that their + initial polygons collectively cover all of some larger polygon, with + no overlap. The individual generators must produce either Cage, or + more CageFork. + """ + def __init__(self, gens): + self.gens = gens + def is_fork(self): + return True + +class CageGen(object): + """A generator, finite or infinite, that produces objects of type Cage. + It can also produce CageFork, but only a single one as the final value + of a finite generator.""" + def __init__(self, gen): + self.gen = gen + def to_mesh(self, count=None, flip_order=False, loop=False, close_first=False, + close_last=False, join_fn=meshutil.join_boundary_simple): + # Get 'opening' polygons of generator: + cage_first = next(self.gen) + if cage_first.is_fork(): + # TODO: Can it be a fork? + raise Exception("First element in CageGen can't be a fork.") + cage_last = cage_first + meshes = [] + # Close off the first polygon if necessary: + if close_first: + for poly in cage_first.polys(): + meshes.append(meshutil.close_boundary_simple(poly)) + # Generate all polygons from there and connect them: + #print(self.gen) + for i, cage_cur in enumerate(self.gen): + #print("{}: {}".format(i, cage_cur)) + if count is not None and i >= count: + break + # If it's a fork, then recursively generate all the geometry + # from them, depth-first: + if cage_cur.is_fork(): + # TODO: Clean up these recursive calls; parameters are ugly. + # Some of them also make no sense in certain combinations + # (e.g. loop with fork) + for gen in cage_cur.gens: + m = gen.to_mesh(count=count - i, flip_order=flip_order, loop=loop, + close_first=close_first, close_last=close_last, + join_fn=join_fn) + meshes.append(m) + # A fork can be only the final element, so disregard anything + # after one and just quit: + break + if flip_order: + for b0,b1 in zip(cage_cur.polys(), cage_last.polys()): + m = join_fn(b0, b1) + meshes.append(m) + else: + for b0,b1 in zip(cage_cur.polys(), cage_last.polys()): + m = join_fn(b1, b0) + meshes.append(m) + cage_last = cage_cur + if loop: + for b0,b1 in zip(cage_last.polys(), cage_first.polys()): + if flip_order: + m = join_fn(b1, b0) + else: + m = join_fn(b0, b1) + meshes.append(m) + # TODO: close_last? + # or should this just look for whether or not the + # generator ends here (without a CageFork)? + mesh = meshutil.FaceVertexMesh.concat_many(meshes) + return mesh diff --git a/examples.py b/examples.py index bff1a16..94f68f4 100755 --- a/examples.py +++ b/examples.py @@ -8,6 +8,7 @@ import trimesh import meshutil import meshgen +import cage # I should be moving some of these things out into more of a # standard library than an 'examples' script @@ -95,6 +96,37 @@ def ram_horn2(): mesh = meshutil.FaceVertexMesh.concat_many(meshes) return mesh +# Rewriting the above rewrite in terms of Cage +def ram_horn3(): + center = meshutil.Transform().translate(-0.5, -0.5, 0) + cage0 = cage.Cage.from_arrays([ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + [0, 1, 0], + ]).transform(center) + xf0_to_1 = meshutil.Transform().translate(0, 0, 1) + cage1 = cage0.transform(xf0_to_1) + opening_boundary = lambda i: meshutil.Transform() \ + .translate(0,0,-1) \ + .scale(0.5) \ + .translate(0.25,0.25,1) \ + .rotate([0,0,1], i*numpy.pi/2) + def recur(xf): + while True: + cage2 = cage1.transform(xf) + yield cage2 + incr = meshutil.Transform() \ + .scale(0.9) \ + .rotate([-1,0,1], 0.3) \ + .translate(0,0,0.8) + xf = incr.compose(xf) + gens = [cage.CageGen(recur(opening_boundary(i))) for i in range(4)] + cg = cage.CageGen(itertools.chain([cage0, cage1, cage.CageFork(gens)])) + # TODO: if this is just a list it seems silly to require itertools + mesh = cg.to_mesh(count=128, close_first=True, close_last=True) + return mesh + def branch_test(): b0 = numpy.array([ [0, 0, 0], @@ -262,6 +294,7 @@ def main(): fns = { ram_horn: "ramhorn.stl", ram_horn2: "ramhorn2.stl", + ram_horn3: "ramhorn3.stl", twist: "twist.stl", twist_nonlinear: "twist_nonlinear.stl", twist_from_gen: "twist_from_gen.stl",