249 lines
7.9 KiB
Python
249 lines
7.9 KiB
Python
import stl.mesh
|
|
import numpy
|
|
import quaternion
|
|
import random
|
|
|
|
import quat
|
|
|
|
# (left/right, bottom/top, back/front)
|
|
# I'm using X to the right, Y up, Z inward
|
|
# (not that it really matters except for variable names)
|
|
lbf = numpy.array([0,0,0])
|
|
rbf = numpy.array([1,0,0])
|
|
ltf = numpy.array([0,1,0])
|
|
rtf = numpy.array([1,1,0])
|
|
lbb = numpy.array([0,0,1])
|
|
rbb = numpy.array([1,0,1])
|
|
ltb = numpy.array([0,1,1])
|
|
rtb = numpy.array([1,1,1])
|
|
|
|
class FaceVertexMesh(object):
|
|
def __init__(self, v, f):
|
|
# v & f should both be of shape (N,3)
|
|
self.v = v
|
|
self.f = f
|
|
def concat(self, other_mesh):
|
|
v2 = numpy.concatenate([self.v, other_mesh.v])
|
|
# Note index shift!
|
|
f2 = numpy.concatenate([self.f, other_mesh.f + self.v.shape[0]])
|
|
m2 = FaceVertexMesh(v2, f2)
|
|
return m2
|
|
def transform(self, xform):
|
|
# Just transform vertices. Indices don't change.
|
|
return FaceVertexMesh(xform.apply_to(self.v), self.f)
|
|
def to_stl_mesh(self):
|
|
data = numpy.zeros(self.f.shape[0], dtype=stl.mesh.Mesh.dtype)
|
|
v = data["vectors"]
|
|
for i, (iv0, iv1, iv2) in enumerate(self.f):
|
|
v[i] = [self.v[iv0], self.v[iv1], self.v[iv2]]
|
|
return stl.mesh.Mesh(data)
|
|
@classmethod
|
|
def Empty(cls):
|
|
return FaceVertexMesh(numpy.zeros((0,3)), numpy.zeros((0,3), dtype=int))
|
|
@classmethod
|
|
def concat_many(cls, meshes):
|
|
nv = 0
|
|
nf = 0
|
|
for m in meshes:
|
|
nv += m.v.shape[0]
|
|
nf += m.f.shape[0]
|
|
v = numpy.zeros((nv,3), dtype=numpy.float64)
|
|
f = numpy.zeros((nf,3), dtype=int)
|
|
vi = 0
|
|
fi = 0
|
|
for m in meshes:
|
|
vj = vi + m.v.shape[0]
|
|
fj = fi + m.f.shape[0]
|
|
v[vi:vj,:] = m.v
|
|
f[fi:fj,:] = m.f + vi
|
|
vi = vj
|
|
fi = fj
|
|
return FaceVertexMesh(v, f)
|
|
|
|
class Transform(object):
|
|
def __init__(self, mtx=None):
|
|
if mtx is None:
|
|
self.mtx = numpy.identity(4)
|
|
else:
|
|
self.mtx = mtx
|
|
def _compose(self, mtx2):
|
|
# Note pre-multiply. Earlier transforms are done first.
|
|
return Transform(mtx2 @ self.mtx)
|
|
def compose(self, xform):
|
|
return self._compose(xform.mtx)
|
|
def scale(self, *a, **kw):
|
|
return self._compose(mtx_scale(*a, **kw))
|
|
def translate(self, *a, **kw):
|
|
return self._compose(mtx_translate(*a, **kw))
|
|
def rotate(self, *a, **kw):
|
|
return self._compose(mtx_rotate(*a, **kw))
|
|
def reflect(self, *a, **kw):
|
|
return self._compose(mtx_reflect(*a, **kw))
|
|
def apply_to(self, vs):
|
|
# Homogeneous coords, so append a column of ones. vh is then shape (N,4):
|
|
vh = numpy.hstack([vs, numpy.ones((vs.shape[0], 1), dtype=vs.dtype)])
|
|
# As we have row vectors, we're doing basically (A*x)^T=(x^T)*(A^T)
|
|
# hence transposing the matrix, while vectors are already transposed.
|
|
return (vh @ self.mtx.T)[:,0:3]
|
|
|
|
def mtx_scale(sx, sy=None, sz=None):
|
|
if sy is None:
|
|
sy = sx
|
|
if sz is None:
|
|
sz = sx
|
|
return numpy.array([
|
|
[sx, 0, 0, 0],
|
|
[0, sy, 0, 0],
|
|
[0, 0, sz, 0],
|
|
[0, 0, 0, 1],
|
|
])
|
|
|
|
def mtx_translate(x, y, z):
|
|
return numpy.array([
|
|
[1, 0, 0, x],
|
|
[0, 1, 0, y],
|
|
[0, 0, 1, z],
|
|
[0, 0, 0, 1],
|
|
])
|
|
|
|
def mtx_rotate(axis, angle):
|
|
q = quat.rotation_quaternion(axis, angle)
|
|
return quat.quat2mat(q)
|
|
|
|
def mtx_reflect(axis):
|
|
# axis must be norm-1
|
|
axis = numpy.array(axis)
|
|
axis = axis / numpy.linalg.norm(axis)
|
|
a,b,c = axis[0], axis[1], axis[2]
|
|
return numpy.array([
|
|
[1-2*a*a, -2*a*b, -2*a*c, 0],
|
|
[-2*a*b, 1-2*b*b, -2*b*c, 0],
|
|
[-2*a*c, -2*b*c, 1-2*c*c, 0],
|
|
[0, 0, 0, 1],
|
|
])
|
|
|
|
def cube(open_xz=False):
|
|
verts = numpy.array([
|
|
lbf, rbf, ltf, rtf,
|
|
lbb, rbb, ltb, rtb,
|
|
], dtype=numpy.float64)
|
|
if open_xz:
|
|
faces = numpy.zeros((8,3), dtype=int)
|
|
else:
|
|
faces = numpy.zeros((12,3), dtype=int)
|
|
faces[0,:] = [0, 3, 1]
|
|
faces[1,:] = [0, 2, 3]
|
|
faces[2,:] = [1, 7, 5]
|
|
faces[3,:] = [1, 3, 7]
|
|
faces[4,:] = [5, 6, 4]
|
|
faces[5,:] = [5, 7, 6]
|
|
faces[6,:] = [4, 2, 0]
|
|
faces[7,:] = [4, 6, 2]
|
|
if not open_xz:
|
|
faces[8,:] = [2, 7, 3]
|
|
faces[9,:] = [2, 6, 7]
|
|
faces[10,:] = [0, 1, 5]
|
|
faces[11,:] = [0, 5, 4]
|
|
# winding order?
|
|
return FaceVertexMesh(verts, faces)
|
|
|
|
def cube_distort(angle, open_xz=False):
|
|
q = quat.rotation_quaternion(numpy.array([-1,0,1]), angle)
|
|
ltf2 = quat.conjugate_by(ltf, q)[0,:]
|
|
rtf2 = quat.conjugate_by(rtf, q)[0,:]
|
|
ltb2 = quat.conjugate_by(ltb, q)[0,:]
|
|
rtb2 = quat.conjugate_by(rtb, q)[0,:]
|
|
# TODO: Just make these functions work right with single vectors
|
|
verts = numpy.array([
|
|
lbf, rbf, ltf2, rtf2,
|
|
lbb, rbb, ltb2, rtb2,
|
|
], dtype=numpy.float64)
|
|
if open_xz:
|
|
faces = numpy.zeros((8,3), dtype=int)
|
|
else:
|
|
faces = numpy.zeros((12,3), dtype=int)
|
|
faces[0,:] = [0, 3, 1]
|
|
faces[1,:] = [0, 2, 3]
|
|
faces[2,:] = [1, 7, 5]
|
|
faces[3,:] = [1, 3, 7]
|
|
faces[4,:] = [5, 6, 4]
|
|
faces[5,:] = [5, 7, 6]
|
|
faces[6,:] = [4, 2, 0]
|
|
faces[7,:] = [4, 6, 2]
|
|
if not open_xz:
|
|
faces[8,:] = [2, 7, 3]
|
|
faces[9,:] = [2, 6, 7]
|
|
faces[10,:] = [0, 1, 5]
|
|
faces[11,:] = [0, 5, 4]
|
|
# winding order?
|
|
return FaceVertexMesh(verts, faces)
|
|
|
|
def split_boundary(bound):
|
|
# assume bound1 has shape (4,3).
|
|
# Midpoints of every segment:
|
|
mids = (bound + numpy.roll(bound, 1, axis=0)) / 2
|
|
mids_adj = numpy.roll(mids, -1, axis=0)
|
|
# Centroid:
|
|
centroid = numpy.mean(bound, axis=0)
|
|
# Now, every single new boundary has: one vertex of 'bound', an
|
|
# adjacent midpoint, a centroid, and the other adjacent midpoint.
|
|
bounds = [
|
|
numpy.array([bound[i,:], mids[i,:], centroid, mids_adj[i,:]])
|
|
for i in range(4)
|
|
]
|
|
return bounds
|
|
|
|
def subdivide_boundary(bound):
|
|
# assume bound1 has shape (4,3).
|
|
# Midpoints of every segment:
|
|
mids = (bound + numpy.roll(bound, -1, axis=0)) / 2
|
|
b2 = numpy.zeros((bound.shape[0]*2, bound.shape[1]))
|
|
for i,row in enumerate(bound):
|
|
b2[2*i,:] = bound[i,:]
|
|
b2[2*i+1,:] = mids[i,:]
|
|
return b2
|
|
|
|
def join_boundary_simple(bound1, bound2, random_diag=False):
|
|
# bound1 & bound2 are both arrays of shape (N,3), representing
|
|
# the points of a boundary. This joins the two boundaries by
|
|
# simply connecting quads (made of 2 triangles) straight across.
|
|
#
|
|
# Winding will proceed in the direction of the first boundary.
|
|
#
|
|
# Returns FaceVertexMesh.
|
|
n = bound1.shape[0]
|
|
vs = numpy.concatenate([bound1, bound2])
|
|
# Indices 0...N-1 are from bound1, N...2*N-1 are from bound2
|
|
fs = numpy.zeros((2*n, 3), dtype=int)
|
|
for i in range(n):
|
|
v0 = i
|
|
v1 = (i + 1) % n
|
|
if random_diag and random.random() < 0.5:
|
|
fs[2*i] = [n + v1, n + v0, v0]
|
|
fs[2*i + 1] = [v1, n + v1, v0]
|
|
else:
|
|
fs[2*i] = [n + v1, n + v0, v1]
|
|
fs[2*i + 1] = [v1, n + v0, v0]
|
|
return FaceVertexMesh(vs, fs)
|
|
|
|
def join_boundary_optim(bound1, bound2):
|
|
# bound1 and bound2 must stay in order, but we can rotate
|
|
# the starting point to whatever we want. Use distance as
|
|
# a metric:
|
|
errs = [numpy.linalg.norm(bound1 - numpy.roll(bound2, i, axis=0))
|
|
for i,_ in enumerate(bound1)]
|
|
# What shift gives the lowest distance?
|
|
i = numpy.argmin(errs)
|
|
return join_boundary_simple(bound1, numpy.roll(bound2, i, axis=0))
|
|
|
|
def close_boundary_simple(bound):
|
|
# This will fail for any non-convex boundary!
|
|
centroid = numpy.mean(bound, axis=0)
|
|
vs = numpy.concatenate([bound, centroid[numpy.newaxis,:]])
|
|
n = bound.shape[0]
|
|
# note that n is new the index of the centroid
|
|
fs = numpy.zeros((n+1, 3), dtype=int)
|
|
for i in range(n):
|
|
fs[i] = [i, n, (i+1) % n]
|
|
return FaceVertexMesh(vs, fs)
|