├── .gitignore ├── .pylintrc ├── LICENSE ├── README.rst ├── anti_lib.py ├── anti_lib_progs ├── __init__.py ├── barrel.py ├── eq_antherm.py ├── geodesic.py ├── gold_bowtie.py ├── lamella.py ├── lat_grid.py ├── njitterbug.py ├── njohnson.py ├── packer.py ├── pentabelt.py ├── proj_dome.py ├── ring_place.py ├── rotegrity_models.py ├── sph_circles.py ├── sph_saff.py ├── sph_spiral.py ├── spiro.py ├── str_art.py ├── temcor_dome.py ├── tri_tiling.py ├── twister.py ├── twister_rhomb.py └── twister_test.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # Rope 57 | .ropeproject 58 | 59 | tmp/ 60 | 61 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [BASIC] 2 | 3 | # Regular expression which should only match correct variable names 4 | variable-rgx=[A-Za-z_][A-Za-z0-9_]*$ 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 Adrian Rossiter 4 | http://www.antiprism.com/ 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | README 2 | ====== 3 | 4 | antiprism_python 5 | ----------------- 6 | 7 | `antiprism_python `_ 8 | is a collection of geometry modelling programs written in Python3, 9 | and associated with the `Antiprism `_ project. 10 | The Antiprism programs may be used to view, process or analyse these models. 11 | 12 | Some of these programs were written to solve a specific problem, 13 | some to solve a general problem, some were written as prototypes. 14 | The programs vary in quality, completeness and usefulness. They are 15 | all shared under the MIT licence. 16 | 17 | Install 18 | ~~~~~~~ 19 | 20 | Install the whole package with ``pip3`` (or ``pip`` if that installs as 21 | Python3), either from the PyPI repository:: 22 | 23 | pip3 install antiprism_python 24 | 25 | Or directly from the Git repository:: 26 | 27 | pip3 install git+git://github.com/antiprism/antiprism_python 28 | 29 | Alternatively, download the programs you are interested in and copy 30 | `anti_lib.py `_ 31 | into the same directory. 32 | 33 | Programs 34 | ~~~~~~~~ 35 | 36 | To see the help for a program, run it with *-h*. 37 | 38 | - `barrel.py `_ 39 | Create a cyclic polyhedron with one or two bands of equatorial 40 | squares, oriented like diamonds. 41 | - `eq_antherm.py `_ 42 | Create an antihermaphrodite with equilateral triangles 43 | - `geodesic.py `_ 44 | Create coordinates for a higher frequency, plane-faced or 45 | spherical, icosahedron, octahedron or tetrahedron. 46 | - `gold_bowtie.py `_ 47 | Create a polyhedron with axial symmetry involving a golden 48 | trapezium bow-tie 49 | - `lat_grid.py `_ 50 | Make a variety of lattices and grids using integer coordinates. 51 | - `lamella.py `_ 52 | Create a lamella domes. Also, makes square barrel models and 53 | multiply-gyroelongated antihermaphrodite models. 54 | - `njitterbug.py `_ 55 | Create a jitterbug model for a general polygon base. The 56 | transformation includes, if constructible, models corresponding to 57 | the antiprism, snub-antiprisms and gyrobicupola for that base. 58 | - `njohnson.py `_ 59 | Create a Johnson-based model, from J88, J89, J90, with a general 60 | polygon base. 61 | - `packer.py `_ 62 | Pack balls in a sphere. The pack is seeded with two or more balls, 63 | then subsequent balls are added one at a time in three point contact 64 | in positions chosen by the packing method. 65 | - `pentabelt.py `_ 66 | Make a model with axial symmetry based on a belt of staggered 67 | pentagons 68 | - `proj_dome.py `_ 69 | Make a Jacoby-style dome, as described in 70 | http://www.google.com/patents/US7900405 . Project a tiling of 71 | unit-edged triangles, squares or crossed squares (unit edges), at 72 | a specified height, onto a unit hemisphere, by gnomonic, 73 | stereographic or general point projection. 74 | - `ring_place.py `_ 75 | Place maximum radius rings of contacting balls around points on a 76 | sphere. Input is a list of coordinates, one set per line. 77 | - `rotegrity_models.py `_ 78 | Create cyclic rotegrity models, with 1 or 2 layers of rotegrity units 79 | - `sph_circles.py `_ 80 | Distribute points on horizontal circles on a sphere (like a disco 81 | ball). The sphere is split into equal width bands. Balls with a 82 | diameter of this width are distributed equally around each band. 83 | The number of balls is either as many points as will fit in the band, 84 | or a specified number. 85 | - `sph_saff.py `_ 86 | Distribute num_points (default 20) on a sphere using the algorithm 87 | from "Distributing many points on a sphere" by E.B. Saff and 88 | A.B.J. Kuijlaars, Mathematical Intelligencer 19.1 (1997) 5--11. 89 | - `sph_spiral.py `_ 90 | Distribute points in a spiral on a sphere. 91 | - `spiro.py `_ 92 | Create a spirograph pattern. 93 | - `str_art.py `_ 94 | Create simple epicycloid string art patterns. 95 | - `temcor_dome.py `_ 96 | Make a Temcor-style dome, using the method described in 97 | https://groups.google.com/d/msg/geodesichelp/hJ3V9Nfp3kE/nikgoBPSFfwJ 98 | . 99 | The base model is a pyramid with a unit edge base polygon at a 100 | specified height above the origin. The axis to rotate the plane 101 | about passes through the origin and is in the direction of the 102 | base polygon mid-edge to the pyramid apex. 103 | - `tri_tiling.py `_ 104 | Create a polyhedron which tiles the sphere with congruent triangles. 105 | - `twister.py `_ 106 | Twist two polygons placed on symmetry axes and joined by a vertex 107 | - `twister_rhomb.py `_ 108 | Twist polygons, of the same type, placed on certain fixed axes and 109 | joined by vertices. 110 | - `twister_test.py `_ 111 | Twist two polygons placed on axes at a specified angle and joined by 112 | a vertex. 113 | 114 | Complementary Programs 115 | ~~~~~~~~~~~~~~~~~~~~~~ 116 | 117 | Related Python programs in external projects 118 | 119 | - `antitile `_ 120 | Generates geodesic models by various methods. 121 | - `view_off.py `_ 122 | An OFF file viewer with export to PNG and SVG. 123 | -------------------------------------------------------------------------------- /anti_lib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | 23 | ''' 24 | anti_lib.py: private library of common code used by programs in this 25 | repository. 26 | ''' 27 | 28 | 29 | import argparse 30 | import fractions 31 | import math 32 | import sys 33 | 34 | 35 | def safe_for_trig(val): 36 | if abs(val) > 1: 37 | return -1 if val < 0 else 1 38 | else: 39 | return val 40 | 41 | 42 | class Vec: 43 | def __init__(self, *v): 44 | self.v = list(v) 45 | 46 | def fromlist(self, v): 47 | if not isinstance(v, list): 48 | raise TypeError 49 | self.v = v[:] 50 | return self 51 | 52 | def copy(self): 53 | return Vec().fromlist(self.v) 54 | 55 | def __str__(self): 56 | return '(' + repr(self.v)[1:-1] + ')' 57 | 58 | def __repr__(self): 59 | return 'Vec(' + repr(self.v)[1:-1] + ')' 60 | 61 | def __len__(self): 62 | return len(self.v) 63 | 64 | def __getitem__(self, key): 65 | if not isinstance(key, int): 66 | raise TypeError 67 | if key < 0 or key >= len(self.v): 68 | raise KeyError 69 | return self.v[key] 70 | 71 | def __setitem__(self, key, value): 72 | if not isinstance(key, int): 73 | raise TypeError 74 | if key < 0 or key >= len(self.v): 75 | raise KeyError 76 | self.v[key] = value 77 | 78 | # Element-wise negation 79 | def __neg__(self): 80 | v = list(map(lambda x: -x, self.v)) 81 | return Vec().fromlist(v) 82 | 83 | # Element-wise addition 84 | def __add__(self, other): 85 | v = list(map(lambda x, y: x+y, self.v, other.v)) 86 | return Vec().fromlist(v) 87 | 88 | # Element-wise subtraction 89 | def __sub__(self, other): 90 | v = list(map(lambda x, y: x-y, self.v, other.v)) 91 | return Vec().fromlist(v) 92 | 93 | # Element-wise multiplication by scalar 94 | def __mul__(self, scalar): 95 | v = list(map(lambda x: x*scalar, self.v)) 96 | return Vec().fromlist(v) 97 | 98 | # Element-wise pre-multiplication by scalar 99 | def __rmul__(self, scalar): 100 | return self.__mul__(scalar) 101 | 102 | # Element-wise division by scalar 103 | def __truediv__(self, scalar): 104 | return self.__mul__(1/scalar) 105 | 106 | # Vector magnitude/length squared 107 | def mag2(self): 108 | return self.dot(self, self) 109 | 110 | # Vector magnitude/length 111 | def mag(self): 112 | return math.sqrt(self.mag2()) 113 | 114 | # Vector as unit 115 | def unit(self): 116 | return self.__truediv__(self.mag()) 117 | 118 | # Vector rotated about z-axis 119 | def rot_z(self, ang): 120 | r = math.sqrt(self.v[0]**2+self.v[1]**2) 121 | initial_ang = math.atan2(self.v[1], self.v[0]) 122 | final_ang = initial_ang + ang 123 | return Vec(r*math.cos(final_ang), r*math.sin(final_ang), self.v[2]) 124 | 125 | # Cross product v0 x v1 126 | @staticmethod 127 | def cross(v0, v1): 128 | return Vec(v1[2]*v0[1] - v1[1]*v0[2], 129 | v1[0]*v0[2] - v1[2]*v0[0], 130 | v1[1]*v0[0] - v1[0]*v0[1]) 131 | 132 | # Dot product v0 . v1 133 | @staticmethod 134 | def dot(v0, v1): 135 | return sum(map(lambda x, y: x*y, v0.v, v1.v)) 136 | 137 | # Triple product v0. (v1 x v2) 138 | @staticmethod 139 | def triple(v0, v1, v2): 140 | return Vec.dot(v0, Vec.cross(v1, v2)) 141 | 142 | 143 | def centroid(v): 144 | return sum(v, Vec(0, 0, 0)) / len(v) 145 | 146 | 147 | class Mat: 148 | def __init__(self, init_type=1): 149 | if init_type == 0: 150 | self.m = [0]*16 151 | elif init_type == 1: 152 | self.m = [0]*16 153 | for i in range(4): 154 | self.m[5*i] = 1 155 | 156 | @staticmethod 157 | def rot_axis_ang(axis, ang): 158 | rot = Mat() 159 | c = math.cos(ang) 160 | s = math.sin(ang) 161 | axis = axis.unit() 162 | t = 1.0 - c 163 | rot.m[0] = c + axis[0]*axis[0]*t 164 | rot.m[5] = c + axis[1]*axis[1]*t 165 | rot.m[10] = c + axis[2]*axis[2]*t 166 | 167 | tmp1 = axis[0]*axis[1]*t 168 | tmp2 = axis[2]*s 169 | rot.m[4] = tmp1 + tmp2 170 | rot.m[1] = tmp1 - tmp2 171 | 172 | tmp1 = axis[0]*axis[2]*t 173 | tmp2 = axis[1]*s 174 | rot.m[8] = tmp1 - tmp2 175 | rot.m[2] = tmp1 + tmp2 176 | 177 | tmp1 = axis[1]*axis[2]*t 178 | tmp2 = axis[0]*s 179 | rot.m[9] = tmp1 + tmp2 180 | rot.m[6] = tmp1 - tmp2 181 | 182 | return rot 183 | 184 | @staticmethod 185 | def rot_xyz(ang_x, ang_y, ang_z): 186 | mat = Mat() 187 | if ang_z: 188 | mat *= Mat.rot_axis_ang(Vec(0, 0, 1), ang_z) 189 | if ang_y: 190 | mat *= Mat.rot_axis_ang(Vec(0, 1, 0), ang_y) 191 | if ang_x: 192 | mat *= Mat.rot_axis_ang(Vec(1, 0, 0), ang_x) 193 | 194 | return mat 195 | 196 | @staticmethod 197 | def rot_from_to(v_from, v_to): 198 | epsilon = 1e-13 199 | n_from = v_from.unit() 200 | n_to = v_to.unit() 201 | axis = Vec.cross(n_from, n_to) 202 | cos_a = safe_for_trig(Vec.dot(n_from, n_to)) 203 | if abs(cos_a) >= 1-epsilon: 204 | axis = Vec.cross(n_from, Vec(1.2135, 2.09865, 3.23784)).unit() 205 | 206 | return Mat.rot_axis_ang(axis, math.acos(cos_a)) 207 | 208 | @staticmethod 209 | def rot_from_to2(from1, from2, to1, to2): 210 | r = (Mat.rot_xyz(0.1, 0, 0) * Mat.rot_xyz(0, 0.2, 0) * 211 | Mat.rot_xyz(0, 0, 0.3)) 212 | inv_r = (Mat.rot_xyz(0, 0, -0.3) * Mat.rot_xyz(0, -0.2, 0) * 213 | Mat.rot_xyz(-0.1, 0, 0)) 214 | to1 = r * to1 215 | to2 = r * to2 216 | 217 | trans = Mat.rot_from_to(from1, to1) 218 | from1 = trans * from1 219 | from2 = trans * from2 220 | 221 | norm1 = Vec.cross(from2, from1).unit() 222 | norm2 = Vec.cross(to2, to1).unit() 223 | 224 | # find the angle to rotate about to1, in the range -180 2: 293 | raise ValueError('fraction has more than one /') 294 | if sz > 1: 295 | try: 296 | D = int(vals[1]) 297 | except Exception: 298 | raise ValueError('fraction denominator is not an integer') 299 | try: 300 | N = int(vals[0]) 301 | except Exception: 302 | raise ValueError('fraction numerator is not an integer') 303 | 304 | self.__init__(N, D) 305 | 306 | 307 | class Polygon(RawFraction): 308 | def __init__(self, N=2, D=None): 309 | if D is None: 310 | D = 1 311 | if N < 0: 312 | N *= -1 313 | D *= -1 314 | if N < 2: 315 | raise ValueError('fraction numerator is less than 2') 316 | 317 | D %= N 318 | if D == 0: 319 | raise ValueError('fraction denominator is a multiple ' 320 | 'of the numerator') 321 | self.parts = math.gcd(N, D) 322 | self.N = N // self.parts 323 | self.D = D // self.parts 324 | 325 | def angle(self): 326 | return 2*math.pi*self.D/self.N 327 | 328 | def circumradius(self, edge=1.0): 329 | return edge / (2*math.sin(self.angle()/2)) 330 | 331 | def inradius(self, edge=1.0): 332 | return edge / (2*math.tan(self.angle()/2)) 333 | 334 | def get_points(self, P=Vec(1, 0, 0)): 335 | return [P.rot_z((j*self.angle() + i*2*math.pi/(self.N*self.parts))) 336 | for i in range(self.parts) for j in range(self.N)] 337 | 338 | def get_faces(self, offset=0): 339 | return [[i+j*self.N+offset for i in range(self.N)] 340 | for j in range(self.parts)] 341 | 342 | 343 | class OffFile: 344 | def __init__(self, strm=sys.stdout): 345 | self.strm = strm 346 | 347 | def print_header(self, num_verts, num_faces): 348 | print('OFF\n{} {} 0'.format(num_verts, num_faces), file=self.strm) 349 | 350 | def print_vert(self, vert): 351 | print(vert[0], vert[1], vert[2], file=self.strm) 352 | 353 | def print_face(self, face, offset=0, col=None): 354 | print(len(face), *[v+offset for v in face], end='', file=self.strm) 355 | if type(col) is int: 356 | print('', col, file=self.strm) 357 | else: 358 | print(file=self.strm) 359 | 360 | def print_verts(self, verts): 361 | for v in verts: 362 | self.print_vert(v) 363 | 364 | def print_faces(self, faces, offset=0, col=None): 365 | for face in faces: 366 | self.print_face(face, offset, col) 367 | 368 | def print_all(self, verts, faces): 369 | self.print_header(len(verts), len(faces)) 370 | self.print_verts(verts) 371 | self.print_faces(faces) 372 | 373 | def print_all_pgon(self, verts, faces, pgon, repeat_side=False): 374 | sides = repeat_side and pgon.N or 1 375 | parts = pgon.parts 376 | self.print_header(len(verts)*sides*parts, len(faces)*sides*parts) 377 | for i in range(parts): 378 | for side in range(sides): 379 | trans = Mat.rot_axis_ang(Vec(0, 0, 1), 2*math.pi*( 380 | side/sides + (i/parts)/pgon.N)) 381 | self.print_verts([trans * v for v in verts]) 382 | 383 | for i in range(parts*sides): 384 | self.print_faces(faces, len(verts)*i) 385 | 386 | 387 | def read_positive_float(str_val): 388 | try: 389 | val = float(str_val) 390 | except Exception: 391 | raise argparse.ArgumentTypeError('not a number') 392 | 393 | if val <= 0.0: 394 | raise argparse.ArgumentTypeError('not a positive number') 395 | 396 | return val 397 | 398 | 399 | def read_positive_int(str_val, min_val=1): 400 | try: 401 | val = int(str_val) 402 | except Exception: 403 | raise argparse.ArgumentTypeError('not an integer') 404 | 405 | if val < min_val: 406 | msgs = ['not zero or greater', 'not a positive integer'] 407 | raise argparse.ArgumentTypeError(msgs[min_val]) 408 | 409 | return val 410 | 411 | 412 | def read_polygon(val_str): 413 | pgon = Polygon() 414 | try: 415 | pgon.read(val_str) 416 | except ValueError as e: 417 | raise argparse.ArgumentTypeError(e.args[0]) 418 | return pgon 419 | 420 | 421 | class DefFormatter(argparse.RawDescriptionHelpFormatter): 422 | '''Allow individual options to be pre-formatted. Description and 423 | epilog are unformatted''' 424 | def _split_lines(self, text, width): 425 | if text.startswith(']'): 426 | return text[1:].splitlines() 427 | return argparse.HelpFormatter._split_lines(self, text, width) 428 | -------------------------------------------------------------------------------- /anti_lib_progs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antiprism/antiprism_python/e329bc87c9568b23a446c17bedd5bc71e73f1cf0/anti_lib_progs/__init__.py -------------------------------------------------------------------------------- /anti_lib_progs/barrel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Create a cyclic polyhedron with one or two bands 24 | of equatorial squares, oriented like diamonds. 25 | ''' 26 | import argparse 27 | import sys 28 | from math import cos, sin, tan, sqrt 29 | import anti_lib 30 | 31 | 32 | def make_1_band_model(pgon, trapezo): 33 | """Make square barrel model with one band of squares""" 34 | N = pgon.N 35 | a = pgon.angle() / 2 36 | R = 1 / sqrt(2) / sin(a) 37 | ht = sqrt(2) 38 | R_ht = 1 / sqrt(2) / tan(a) 39 | points = [anti_lib.Vec(0, 0, 0)] * 3 * N 40 | for i in range(N): 41 | points[i] = anti_lib.Vec( 42 | R_ht * cos(2 * i * a), R_ht * sin(2 * i * a), ht / 2) 43 | points[i + N] = anti_lib.Vec(R * cos(2 * i * a + a), 44 | R * sin(2 * i * a + a), 0) 45 | points[i + 2 * N] = anti_lib.Vec(R_ht * cos(2 * i * a), 46 | R_ht * sin(2 * i * a), -ht / 2) 47 | 48 | if trapezo: 49 | A = sqrt(points[N][0]**2 + points[N][1]**2) 50 | mid = anti_lib.centroid([points[0], points[1]]) 51 | B = sqrt(mid[0]**2 + mid[1]**2) 52 | z_diff = mid[2] - points[N][2] 53 | apex = points[N][2] - A * z_diff / (B - A) 54 | points.append(anti_lib.Vec(0, 0, apex)) 55 | points.append(anti_lib.Vec(0, 0, -apex)) 56 | 57 | faces = [] 58 | if not trapezo: 59 | faces.append([i for i in range(N)]) # top 60 | faces.append([i + 2 * N for i in range(N)]) # bottom 61 | 62 | for i in range(N): 63 | faces.append([i, i + N, i + 2 * N, (i - 1 + N) % N + N]) 64 | if trapezo: 65 | faces.append([i, 3 * N, (i + 1) % N, i + N]) 66 | faces.append([i + N, (i + 1) % N + 2 * N, 3 * N + 1, i + 2 * N]) 67 | else: 68 | faces.append([i, (i + 1) % N, i + N]) 69 | faces.append([i + N, (i + 1) % N + 2 * N, i + 2 * N]) 70 | 71 | return points, faces 72 | 73 | 74 | def make_2_band_model(pgon, trapezo): 75 | """Make square barrel model with two bands of squares""" 76 | N = pgon.N 77 | a = pgon.angle() / 2 78 | R = 1 / (sqrt(2) * sin(a)) 79 | ht = sqrt(1 - 0.5 / (cos(a / 2))**2) 80 | points = [anti_lib.Vec(0, 0, 0)] * 4 * N 81 | for i in range(N): 82 | points[i] = anti_lib.Vec( 83 | R * cos(2 * i * a + a / 2), R * sin(2 * i * a + a / 2), ht / 2) 84 | points[i + N] = anti_lib.Vec(R * cos(2 * i * a - a / 2), 85 | R * sin(2 * i * a - a / 2), -ht / 2) 86 | 87 | for i in range(N): 88 | # Add in the fourth point of the squares 89 | points[i + 2 * N] = points[i] - \ 90 | points[(i + 1) % N + N] + points[(i + 1) % N] 91 | points[i + 3 * N] = points[i + N] - points[i] + points[(i + 1) % N + N] 92 | 93 | if trapezo: 94 | A = sqrt(points[1][0]**2 + points[1][1]**2) 95 | mid = (points[2 * N] + points[2 * N + 1]) / 2 96 | B = (1 - 2 * (N < 4 * pgon.D)) * sqrt(mid[0]**2 + mid[1]**2) 97 | z_diff = mid[2] - points[1][2] 98 | apex = points[1][2] - A * z_diff / (B - A) 99 | points.append(anti_lib.Vec(0, 0, apex)) 100 | points.append(anti_lib.Vec(0, 0, -apex)) 101 | 102 | faces = [] 103 | if not trapezo: 104 | faces.append([i + 2 * N for i in range(N)]) # top 105 | faces.append([i + 3 * N for i in range(N)]) # bottom 106 | 107 | for i in range(N): 108 | faces.append([i, (i + 1) % N + N, (i + 1) % N, i + 2 * N]) 109 | faces.append([i + N, i, (i + 1) % N + N, i + 3 * N]) 110 | if trapezo: 111 | faces.append([i + 3 * N, (i + 1) % N + N, (i + 1) % 112 | N + 3 * N, 4 * N + 1]) 113 | faces.append([i + 2 * N, (i + 1) % N, (i + 1) % N + 2 * N, 4 * N]) 114 | else: 115 | faces.append([i + 3 * N, (i + 1) % N + N, (i + 1) % N + 3 * N]) 116 | faces.append([i + 2 * N, (i + 1) % N, (i + 1) % N + 2 * N]) 117 | 118 | return points, faces 119 | 120 | 121 | def main(): 122 | """Entry point""" 123 | epilog = ''' 124 | notes: 125 | Depends on anti_lib.py. 126 | 127 | examples: 128 | Cuboctahedron 129 | barrel.py 4 | antiview 130 | 131 | Pentagonal square barrel capped with pyramids ("trapezobarrel") 132 | barrel.py -t 5 | antiview 133 | 134 | Pentagonal square barrel with two bands of squares 135 | barrel.py -n 2 5 | antiview 136 | ''' 137 | 138 | parser = argparse.ArgumentParser(formatter_class=anti_lib.DefFormatter, 139 | description=__doc__, epilog=epilog) 140 | 141 | parser.add_argument( 142 | 'polygon_fraction', 143 | help='number of sides of the base polygon (N), ' 144 | 'or a fraction for star polygons (N/D) (default: 6)', 145 | default='6', 146 | nargs='?', 147 | type=anti_lib.read_polygon) 148 | parser.add_argument( 149 | '-n', '--number-bands', 150 | help='number of bands of squares (default: 1)', 151 | choices=['1', '2'], 152 | default='1') 153 | parser.add_argument( 154 | '-t', '--trapezo', 155 | help='make a "trapezobarrel" by extending the triangles ' 156 | 'into kites (like a trapezohedron can be made from ' 157 | 'an antiprism)', 158 | action='store_true') 159 | parser.add_argument( 160 | '-o', '--outfile', 161 | help='output file name (default: standard output)', 162 | type=argparse.FileType('w'), 163 | default=sys.stdout) 164 | 165 | args = parser.parse_args() 166 | 167 | pgon = args.polygon_fraction 168 | if args.number_bands == '1': 169 | points, faces = make_1_band_model(pgon, args.trapezo) 170 | else: 171 | if pgon.N < 2 * pgon.D: 172 | parser.error('polyhedron is not constructible (fraction > 1/2)') 173 | points, faces = make_2_band_model(pgon, args.trapezo) 174 | 175 | out = anti_lib.OffFile(args.outfile) 176 | out.print_all_pgon(points, faces, pgon) 177 | 178 | 179 | if __name__ == "__main__": 180 | main() 181 | -------------------------------------------------------------------------------- /anti_lib_progs/eq_antherm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Create an antihermaphrodite with equilateral triangles 24 | ''' 25 | 26 | import argparse 27 | import sys 28 | from math import cos, sin, tan, sqrt, radians 29 | import anti_lib 30 | from anti_lib import Vec 31 | 32 | 33 | def make_equ_antiherm(pgon, dih_ang): 34 | """Make a hermaphrodite with equilateral triangles""" 35 | N = pgon.N 36 | a = pgon.angle()/2 37 | R = pgon.circumradius() 38 | tri_alt = sqrt(3)/2 39 | tri_ht = tri_alt * sin(dih_ang) 40 | R2 = pgon.inradius() + tri_alt * cos(dih_ang) 41 | 42 | points = [Vec(0, 0, 0)]*(2*N+1) 43 | for i in range(N): 44 | points[i] = Vec(R*cos(2*i*a), R*sin(2*i*a), 0) 45 | points[i+N] = Vec(R2*cos(2*i*a+a), R2*sin(2*i*a+a), tri_ht) 46 | 47 | A = sqrt(points[0][0]**2 + points[0][1]**2) 48 | mid = anti_lib.centroid([points[N], points[2*N-1]]) 49 | B = sqrt(mid[0]**2 + mid[1]**2) 50 | z_diff = mid[2] - points[0][2] 51 | points[2*N][2] = A * z_diff / (A - B) 52 | 53 | faces = [] 54 | faces.append([i for i in range(N)]) # bottom 55 | 56 | for i in range(N): 57 | faces.append([i, (i + 1) % N, N + i]) 58 | faces.append([N + i, 2*N, N + (i + 1) % N, (i + 1) % N]) 59 | 60 | return points, faces 61 | 62 | 63 | def main(): 64 | """Entry point""" 65 | 66 | epilog = ''' 67 | notes: 68 | Depends on anti_lib.py. 69 | 70 | examples: 71 | Pentagonal hermaphrodite with equilateral triangles angled at 100 degrees 72 | from base 73 | eq_antherm.py -a 100 5 | antiview 74 | ''' 75 | 76 | parser = argparse.ArgumentParser(formatter_class=anti_lib.DefFormatter, 77 | description=__doc__, epilog=epilog) 78 | 79 | parser.add_argument( 80 | 'polygon_fraction', 81 | help='number of sides of the base polygon (N), ' 82 | 'or a fraction for star polygons (N/D) (default: 6)', 83 | default='6', 84 | nargs='?', 85 | type=anti_lib.read_polygon) 86 | parser.add_argument( 87 | '-a', '--angle', 88 | help='dihedral angle at base (default: 90)', 89 | type=float, 90 | default='90') 91 | parser.add_argument( 92 | '-o', '--outfile', 93 | help='output file name (default: standard output)', 94 | type=argparse.FileType('w'), 95 | default=sys.stdout) 96 | 97 | args = parser.parse_args() 98 | 99 | pgon = args.polygon_fraction 100 | dih = radians(args.angle) 101 | 102 | points, faces = make_equ_antiherm(pgon, dih) 103 | 104 | out = anti_lib.OffFile(args.outfile) 105 | out.print_all_pgon(points, faces, pgon) 106 | 107 | c = sqrt(3)/6 108 | o = pgon.inradius() + c*cos(dih) 109 | h = o/tan(dih) + c*sin(dih) 110 | print("0,0,%f" % (h), file=sys.stderr) 111 | 112 | if __name__ == "__main__": 113 | main() 114 | -------------------------------------------------------------------------------- /anti_lib_progs/geodesic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2003-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Create coordinates for a higher frequency, plane-faced or spherical, 24 | icosahedron, octahedron or tetrahedron. For Class I and II patterns 25 | freq (default 1) is the number of divisions along an edge, for Class III 26 | patterns (and those specified by two numbers) freq is the number of times 27 | the pattern is repeated along an edge. By default the edges are divided 28 | into sections with an equal angle at the origin, a Class I pattern, and 29 | the points are then projected onto a sphere. 30 | ''' 31 | 32 | import argparse 33 | import sys 34 | import math 35 | import fractions 36 | import anti_lib 37 | from anti_lib import Vec 38 | 39 | 40 | def get_octahedron(verts, faces): 41 | """Return an octahedron""" 42 | X = 0.25 * math.sqrt(2) 43 | verts.extend([Vec(0.0, 0.5, 0.0), Vec(X, 0.0, -X), 44 | Vec(X, 0.0, X), Vec(-X, 0.0, X), 45 | Vec(-X, 0.0, -X), Vec(0.0, -0.5, 0.0)]) 46 | 47 | faces.extend([(0, 1, 2), (0, 2, 3), (0, 3, 4), (0, 4, 1), 48 | (5, 2, 1), (2, 5, 3), (3, 5, 4), (4, 5, 1)]) 49 | 50 | 51 | def get_tetrahedron(verts, faces): 52 | """Return an tetrahedron""" 53 | X = 1 / math.sqrt(3) 54 | verts.extend([Vec(-X, X, -X), Vec(-X, -X, X), 55 | Vec(X, X, X), Vec(X, -X, -X)]) 56 | faces.extend([(0, 1, 2), (0, 3, 1), (0, 2, 3), (2, 1, 3)]) 57 | 58 | 59 | def get_ico_coords(): 60 | """Return icosahedron coordinate values""" 61 | phi = (math.sqrt(5) + 1) / 2 62 | rad = math.sqrt(phi+2) 63 | return 1/rad, phi/rad 64 | 65 | 66 | def get_triangle(verts, faces): 67 | """Return an triangle""" 68 | if 1: 69 | Y = math.sqrt(3.0) / 12.0 70 | Z = -0.8 71 | verts.extend([Vec(-0.25, -Y, Z), Vec(0.25, -Y, Z), 72 | Vec(0.0, 2 * Y, Z)]) 73 | faces.extend([(0, 1, 2)]) 74 | else: 75 | X, Z = get_ico_coords() 76 | verts.extend([Vec(-X, 0.0, -Z), Vec(X, 0.0, -Z), 77 | Vec(0.0, Z, -X), Vec(0.0, -Z, -X)]) 78 | faces.extend([(0, 1, 2), (0, 3, 1)]) 79 | 80 | 81 | def get_icosahedron(verts, faces): 82 | """Return an icosahedron""" 83 | X, Z = get_ico_coords() 84 | verts.extend([Vec(-X, 0.0, Z), Vec(X, 0.0, Z), Vec(-X, 0.0, -Z), 85 | Vec(X, 0.0, -Z), Vec(0.0, Z, X), Vec(0.0, Z, -X), 86 | Vec(0.0, -Z, X), Vec(0.0, -Z, -X), Vec(Z, X, 0.0), 87 | Vec(-Z, X, 0.0), Vec(Z, -X, 0.0), Vec(-Z, -X, 0.0)]) 88 | 89 | faces.extend([(0, 4, 1), (0, 9, 4), (9, 5, 4), (4, 5, 8), (4, 8, 1), 90 | (8, 10, 1), (8, 3, 10), (5, 3, 8), (5, 2, 3), (2, 7, 3), 91 | (7, 10, 3), (7, 6, 10), (7, 11, 6), (11, 0, 6), (0, 1, 6), 92 | (6, 1, 10), (9, 0, 11), (9, 11, 2), (9, 2, 5), (7, 2, 11)]) 93 | 94 | 95 | def get_poly(poly, verts, edges, faces): 96 | """Return the base polyhedron""" 97 | if poly == 'i': 98 | get_icosahedron(verts, faces) 99 | elif poly == 'o': 100 | get_octahedron(verts, faces) 101 | elif poly == 't': 102 | get_tetrahedron(verts, faces) 103 | elif poly == 'T': 104 | get_triangle(verts, faces) 105 | else: 106 | return 0 107 | 108 | for face in faces: 109 | for i in range(0, len(face)): 110 | i2 = i + 1 111 | if(i2 == len(face)): 112 | i2 = 0 113 | 114 | if face[i] < face[i2]: 115 | edges[(face[i], face[i2])] = 0 116 | else: 117 | edges[(face[i2], face[i])] = 0 118 | 119 | return 1 120 | 121 | 122 | def grid_to_points(grid, freq, div_by_len, f_verts, face): 123 | """Convert grid coordinates to Cartesian coordinates""" 124 | points = [] 125 | v = [] 126 | for vtx in range(3): 127 | v.append([Vec(0.0, 0.0, 0.0)]) 128 | edge_vec = f_verts[(vtx + 1) % 3] - f_verts[vtx] 129 | if div_by_len: 130 | for i in range(1, freq + 1): 131 | v[vtx].append(edge_vec * float(i) / freq) 132 | else: 133 | ang = 2 * math.asin(edge_vec.mag() / 2.0) 134 | unit_edge_vec = edge_vec.unit() 135 | for i in range(1, freq + 1): 136 | len = math.sin(i * ang / freq) / \ 137 | math.sin(math.pi / 2 + ang / 2 - i * ang / freq) 138 | v[vtx].append(unit_edge_vec * len) 139 | 140 | for (i, j) in grid.values(): 141 | 142 | if (i == 0) + (j == 0) + (i + j == freq) == 2: # skip vertex 143 | continue 144 | # skip edges in one direction 145 | if (i == 0 and face[2] > face[0]) or ( 146 | j == 0 and face[0] > face[1]) or ( 147 | i + j == freq and face[1] > face[2]): 148 | continue 149 | 150 | n = [i, j, freq - i - j] 151 | v_delta = (v[0][n[0]] + v[(0-1) % 3][freq - n[(0+1) % 3]] - 152 | v[(0-1) % 3][freq]) 153 | pt = f_verts[0] + v_delta 154 | if not div_by_len: 155 | for k in [1, 2]: 156 | v_delta = (v[k][n[k]] + v[(k-1) % 3][freq - n[(k+1) % 3]] - 157 | v[(k-1) % 3][freq]) 158 | pt = pt + f_verts[k] + v_delta 159 | pt = pt / 3 160 | points.append(pt) 161 | 162 | return points 163 | 164 | 165 | def make_grid(freq, m, n): 166 | """Make the geodesic pattern grid""" 167 | grid = {} 168 | rng = (2 * freq) // (m + n) 169 | for i in range(rng): 170 | for j in range(rng): 171 | x = i * (-n) + j * (m + n) 172 | y = i * (m + n) + j * (-m) 173 | 174 | if x >= 0 and y >= 0 and x + y <= freq: 175 | grid[(i, j)] = (x, y) 176 | 177 | return grid 178 | 179 | 180 | def class_type(val_str): 181 | """Read the class pattern specifier""" 182 | order = ['first', 'second'] 183 | num_parts = val_str.count(',')+1 184 | vals = val_str.split(',', 2) 185 | if num_parts == 1: 186 | if vals[0] == '1': 187 | pat = [1, 0, 1] 188 | elif vals[0] == '2': 189 | pat = [1, 1, 1] 190 | else: 191 | raise argparse.ArgumentTypeError( 192 | 'class type can only be 1 or 2 when a single value is given') 193 | 194 | elif num_parts == 2: 195 | pat = [] 196 | for i, num_str in enumerate(vals): 197 | try: 198 | num = int(num_str) 199 | except: 200 | raise argparse.ArgumentTypeError( 201 | order[i] + ' class pattern value not an integer') 202 | if num < 0: 203 | raise argparse.ArgumentTypeError( 204 | order[i] + ' class pattern cannot be negative') 205 | if num == 0 and i == 1 and pat[0] == 0: 206 | raise argparse.ArgumentTypeError( 207 | ' class pattern values cannot both be 0') 208 | pat.append(num) 209 | 210 | rep = math.gcd(*pat) 211 | pat = [pat_num//rep for pat_num in pat] 212 | pat.append(rep) 213 | 214 | else: 215 | raise argparse.ArgumentTypeError( 216 | 'class type contains more than two values') 217 | 218 | return pat 219 | 220 | 221 | def main(): 222 | """Entry point""" 223 | epilog = ''' 224 | notes: 225 | Depends on anti_lib.py. Use Antiprism conv_hull to create faces for 226 | convex models (larger frequency tetrahdral geodesic spheres tend to 227 | be non-convex). 228 | 229 | examples: 230 | Icosahedral Class I F10 geodesic sphere 231 | geodesic.py 10 | conv_hull | antiview 232 | 233 | Octahedral Class 2 geodesic sphere 234 | geodesic.py -p o -c 2 10 | conv_hull | antiview 235 | 236 | Icosahedral Class 3 [3,1] geodesic sphere 237 | geodesic.py -c 3,1 | conv_hull | antiview 238 | 239 | Flat-faced equal-length division tetrahedral model 240 | geodesic.py -p t -f -l -c 5,2 | conv_hull -a | antiview -v 0.05 241 | 242 | ''' 243 | 244 | parser = argparse.ArgumentParser(formatter_class=anti_lib.DefFormatter, 245 | description=__doc__, epilog=epilog) 246 | 247 | parser.add_argument( 248 | 'repeats', 249 | help='number of times the pattern is repeated (default: 1)', 250 | type=anti_lib.read_positive_int, 251 | nargs='?', 252 | default=1) 253 | parser.add_argument( 254 | '-p', '--polyhedron', 255 | help='base polyhedron: i - icosahedron (default), ' 256 | 'o - octahedron, t - tetrahedron, T - triangle.', 257 | choices=['i', 'o', 't', 'T'], 258 | default='i') 259 | parser.add_argument( 260 | '-c', '--class-pattern', 261 | help='class of face division, 1 (Class I, default) or ' 262 | '2 (Class II), or two numbers separated by a comma to ' 263 | 'determine the pattern (Class III generally, but 1,0 is ' 264 | 'Class I, 1,1 is Class II, etc).', 265 | type=class_type, 266 | default=[1, 0, 1]) 267 | parser.add_argument( 268 | '-f', '--flat-faced', 269 | help='keep flat-faced polyhedron rather than projecting ' 270 | 'the points onto a sphere.', 271 | action='store_true') 272 | parser.add_argument( 273 | '-l', '--equal-length', 274 | help='divide the edges by equal lengths rather than equal angles', 275 | action='store_true') 276 | parser.add_argument( 277 | '-o', '--outfile', 278 | help='output file name (default: standard output)', 279 | type=argparse.FileType('w'), 280 | default=sys.stdout) 281 | 282 | args = parser.parse_args() 283 | 284 | verts = [] 285 | edges = {} 286 | faces = [] 287 | get_poly(args.polyhedron, verts, edges, faces) 288 | 289 | (M, N, reps) = args.class_pattern 290 | repeats = args.repeats * reps 291 | freq = repeats * (M**2 + M*N + N**2) 292 | 293 | grid = {} 294 | grid = make_grid(freq, M, N) 295 | 296 | points = verts 297 | for face in faces: 298 | if args.polyhedron == 'T': 299 | face_edges = (0, 0, 0) # generate points for all edges 300 | else: 301 | face_edges = face 302 | points[len(points):len(points)] = grid_to_points( 303 | grid, freq, args.equal_length, 304 | [verts[face[i]] for i in range(3)], face_edges) 305 | 306 | if not args.flat_faced: 307 | points = [p.unit() for p in points] # Project onto sphere 308 | 309 | out = anti_lib.OffFile(args.outfile) 310 | out.print_verts(points) 311 | 312 | if __name__ == "__main__": 313 | main() 314 | -------------------------------------------------------------------------------- /anti_lib_progs/gold_bowtie.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Create a polyhedron with axial symmetry involving a golden trapezium bow-tie 24 | ''' 25 | 26 | import argparse 27 | import sys 28 | import math 29 | from math import cos, sin, tan, sqrt 30 | import anti_lib 31 | from anti_lib import Vec 32 | 33 | epsilon = 1e-13 34 | phi = (sqrt(5) + 1) / 2 35 | gold_trap_ht = sqrt(5*phi + 2) / 2 36 | gold_trap_diag = sqrt(2*phi + 1) 37 | 38 | 39 | def get_belt_data(pgon, a, b): 40 | """Get belt construction values""" 41 | ang = pgon.angle()/2 42 | tan_a = a*sin(ang)/(a*cos(ang)+b) # tan of half of edge a 43 | r_a = a/(2*tan_a) # radius to middle of edge a 44 | R_a = math.sqrt((a/2)**2 + r_a**2) # circumradius 45 | return tan_a, r_a, R_a 46 | 47 | 48 | def cup_model(pgon, model_type): 49 | """Construct cupula model with golden trapziums""" 50 | ang = pgon.angle()/2 51 | if model_type in ['0']: 52 | len_a, len_b, len_c = (phi-1)/sin(math.pi/2 - ang), 1, phi 53 | elif model_type in ['1', '2']: 54 | len_a, len_b, len_c = phi, 1, phi 55 | elif model_type in ['3']: 56 | len_a, len_b, len_c = phi, phi, 1 57 | ang = pgon.angle()/2 58 | tan_a, r_a, R_a = get_belt_data(pgon, len_a, len_b) 59 | P = Vec(r_a, -len_a/2, 0) 60 | Q = P + Vec(0, len_a, 0) 61 | points = [] 62 | for i in range(pgon.N): 63 | points.append(P.rot_z(2*i*ang)) 64 | points.append(Q.rot_z(2*i*ang)) 65 | 66 | tan_b, r_b, R_b = get_belt_data(pgon, len_b, len_a) 67 | r_c = len_c/(2*tan(ang)) 68 | ht2 = gold_trap_ht**2 - (r_b - r_c)**2 69 | # print(ht2, file=sys.stderr) 70 | if ht2 < -epsilon: 71 | raise ValueError('model is not constructible (height)') 72 | elif ht2 < 0: 73 | ht2 = 0 74 | ht = math.sqrt(ht2) 75 | R_c = len_c/(2*sin(ang)) 76 | V = Vec(R_c, 0, ht) 77 | N = pgon.N 78 | faces = [] 79 | if model_type not in ['3']: 80 | faces.append([2*N + i for i in range(N)]) 81 | for i in range(pgon.N): 82 | points.append(V.rot_z(2*i*ang)) 83 | if model_type in ['0', '1']: 84 | faces.append([3*N + i for i in range(N)]) 85 | U = Vec(R_c, 0, -ht) 86 | for i in range(pgon.N): 87 | points.append(U.rot_z(2*i*ang)) 88 | elif model_type in ['3']: 89 | faces.append([i for i in range(2*N)]) 90 | 91 | for i in range(N): 92 | if model_type in ['0']: 93 | faces.append([2*i, 3*N+i, 2*i+1, 2*N+i]) 94 | else: 95 | faces.append([2*i, 2*i+1, 2*N+i]) 96 | faces.append([2*i+1, (2*i+2) % (2*N), 97 | 2*N + ((i+1) % N), 2*N + i]) 98 | 99 | if model_type in ['0', '1']: 100 | for i in range(N): 101 | if model_type in ['1']: 102 | faces.append([2*i, 2*i+1, 3*N+i]) 103 | faces.append([2*i+1, (2*i+2) % (2*N), 104 | 3*N + ((i+1) % N), 3*N + i]) 105 | 106 | elif model_type in ['2']: 107 | belt_ht2 = gold_trap_ht**2 - (r_b - r_a)**2 108 | if belt_ht2 < -epsilon: 109 | raise ValueError('model is not constructible (belt height)') 110 | elif belt_ht2 < 0: 111 | belt_ht2 = 0 112 | belt_ht = math.sqrt(belt_ht2) 113 | for i, p in enumerate(points): 114 | points[i][2] += belt_ht/2 115 | points += [Vec(p[0], p[1], -p[2]).rot_z(ang) for p in points] 116 | faces += [[idx+3*N for idx in f] for f in faces] 117 | for i in range(2*N): 118 | faces.append([i, (i+1) % (2*N), 3*N + i, 3*N + (2*N+i-1) % (2*N)]) 119 | 120 | elif model_type in ['3']: 121 | for i, p in enumerate(points): 122 | points[i][2] -= ht 123 | points += [Vec(p[0], p[1], -p[2]) for p in points[:2*N]] 124 | 125 | def new_v(v): v+3*N if v < 2*N else v 126 | 127 | for i in range(len(faces)): 128 | faces.append([new_v(v) for v in faces[i]]) 129 | 130 | return points, faces 131 | 132 | 133 | def bifrustum_model(pgon, arg_type): 134 | """Construct bifrustum model with golden trapziums""" 135 | N = pgon.N 136 | ang = pgon.angle()/2 137 | R0 = phi/(2*sin(ang)) 138 | R1 = 1/(2*sin(ang)) 139 | ht = sqrt(phi**2 - (R0-R1)**2) 140 | points = [] 141 | points += [Vec(R0, 0, ht).rot_z(2*i*ang) for i in range(N)] 142 | points += [Vec(R1, 0, 0).rot_z(2*i*ang) for i in range(N)] 143 | points += [Vec(R0, 0, -ht).rot_z(2*i*ang) for i in range(N)] 144 | 145 | faces = [] 146 | faces.append([i for i in range(N)]) 147 | faces.append([i+2*N for i in range(N)]) 148 | for i in range(N): 149 | faces.append([i, (i+1) % N, N + (i+1) % N, N + i]) 150 | faces.append([N + i, N + (i+1) % N, 2*N + (i+1) % N, 2*N + i]) 151 | return points, faces 152 | 153 | 154 | def tri_antiprism_pt(pgon, a, b, c): 155 | """Return construct point for antiprism model""" 156 | s = (a + b + c) / 2 157 | alt = 2*math.sqrt(s*(s-a)*(s-b)*(s-c)) / a # planar height of A 158 | ang = pgon.angle()/2 159 | R = pgon.circumradius(a) # polygon circumradius (side a) 160 | r = pgon.inradius(a) # polygon inradius (side a) 161 | a0 = math.sqrt(b**2 - alt**2) # from C to perpendicular from A 162 | y = a0 - a/2 163 | x = math.sqrt(R**2 - y**2) 164 | alt_proj_x = x - r # projection of alt onto x dir 165 | z = math.sqrt(alt**2 - alt_proj_x**2) / 2 166 | ang_to_A = math.atan2(y, x) # adjust point for dih axis 167 | ang_off = (ang_to_A - ang)/2 168 | return Vec(x, y, z).rot_z(-ang_off) 169 | 170 | 171 | def antiprism_model(pgon, arg_type): 172 | """Construct antiprism model with golden trapziums""" 173 | N = pgon.N 174 | ang = pgon.angle()/2 175 | P = tri_antiprism_pt(pgon, gold_trap_diag, 1, phi) 176 | Q = Vec(P[0], -P[1], -P[2]) 177 | points = [] 178 | for i in range(N): 179 | points.append(P.rot_z(2*i*ang)) 180 | for i in range(N): 181 | points.append(Q.rot_z(2*i*ang)) 182 | R = points[N-1] + (P - Q)*phi 183 | for i in range(N): 184 | points.append(R.rot_z(2*i*ang)) 185 | S = Vec(R[0], -R[1], -R[2]) 186 | for i in range(N): 187 | points.append(S.rot_z(2*i*ang)) 188 | 189 | faces = [] 190 | faces.append([2*N + i for i in range(N)]) 191 | faces.append([3*N + i for i in range(N)]) 192 | for i in range(N): 193 | faces.append([i, 2*N + i, 2*N + ((i+1) % N)]) 194 | faces.append([N+i, 3*N + i, 3*N + ((i-1) % N)]) 195 | faces.append([i, 2*N + ((i+1) % N), 196 | ((i+1) % N), N + ((i+1) % N)]) 197 | faces.append([i, N + ((i+1) % N), 3*N + i, N + i]) 198 | return points, faces 199 | 200 | 201 | def main(): 202 | """Entry point""" 203 | epilog = ''' 204 | notes: 205 | Depends on anti_lib.py. Not all models are constructible. Use of golden 206 | trapeziums in polyhedra proposed by Dave Smith - 207 | https://hedraweb.wordpress.com/ 208 | 209 | examples: 210 | Pentagonal orthobicupola type 211 | gold_bowtie.py 5 1 | antiview 212 | 213 | Petagramatic elongated gyrobicupola type 214 | gold_bowtie.py 5/2 2 | antiview 215 | 216 | ''' 217 | 218 | parser = argparse.ArgumentParser(formatter_class=anti_lib.DefFormatter, 219 | description=__doc__, epilog=epilog) 220 | 221 | parser.add_argument( 222 | 'polygon_fraction', 223 | help='number of sides of the base polygon (N), ' 224 | 'or a fraction for star polygons (N/D) (default: 5)', 225 | default='5', 226 | nargs='?', 227 | type=anti_lib.read_polygon) 228 | parser.add_argument( 229 | 'poly_type', 230 | help=''']polyhedron type (default: 0): 231 | 0 - like a square barrel polyhedron 232 | 1 - orthobicupola 233 | 2 - elongated gyrobicupola 234 | 3 - inverted bicupola 235 | 4 - inverted bifrustum 236 | 5 - like a double square barrel polyhedron''', 237 | choices=['0', '1', '2', '3', '4', '5'], 238 | default='1', 239 | nargs='?') 240 | parser.add_argument( 241 | '-o', '--outfile', 242 | help='output file name (default: standard output)', 243 | type=argparse.FileType('w'), 244 | default=sys.stdout) 245 | 246 | args = parser.parse_args() 247 | pgon = args.polygon_fraction 248 | 249 | if args.poly_type in ['0', '1', '2', '3']: 250 | points, faces = cup_model(pgon, args.poly_type) 251 | elif args.poly_type == '4': 252 | points, faces = bifrustum_model(pgon, args.poly_type) 253 | elif args.poly_type == '5': 254 | points, faces = antiprism_model(pgon, args.poly_type) 255 | else: 256 | parser.error('unknown polyhedron type') 257 | 258 | out = anti_lib.OffFile(args.outfile) 259 | out.print_all_pgon(points, faces, pgon) 260 | 261 | if __name__ == "__main__": 262 | main() 263 | -------------------------------------------------------------------------------- /anti_lib_progs/lamella.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2017 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Create a lamella dome with a constant kite angle. 24 | ''' 25 | 26 | import argparse 27 | import sys 28 | from math import cos, sin, tan, sqrt, atan, acos, pi 29 | import anti_lib 30 | from anti_lib import Vec 31 | 32 | epsilon = 1e-13 33 | 34 | 35 | def get_default_angle(pgon, radius_top, radius_bottom, height): 36 | a = pgon.angle()/2 37 | P1 = Vec(radius_top, 0, height) 38 | Q1 = Vec(radius_bottom * cos(a), radius_bottom * sin(a), 0.0) 39 | Q2 = Vec(radius_bottom * cos(-a), radius_bottom * sin(-a), 0.0) 40 | v0 = (Q1-P1).unit() 41 | v1 = (Q2-P1).unit() 42 | cos_a = anti_lib.safe_for_trig(Vec.dot(v0, v1)) 43 | ang = acos(cos_a) 44 | return pi-ang 45 | 46 | 47 | def get_axis_intersect(P0, P1, P2): 48 | mid = (P0 + P1)/2 49 | A = sqrt(P2[0]**2 + P2[1]**2) 50 | B = sqrt(mid[0]**2 + mid[1]**2) 51 | z_diff = mid[2] - P2[2] 52 | apex = P2[2] - A * z_diff / (B - A) 53 | return anti_lib.Vec(0, 0, apex) 54 | 55 | 56 | def get_next_radius_height(vals0, vals1, ang, k_ang): 57 | R0 = vals0[0] 58 | h0 = vals0[1] 59 | R1 = vals1[0] 60 | h1 = vals1[1] 61 | h = h1 - h0 62 | r1 = R1*cos(ang) 63 | d = R0 - r1 64 | l = sqrt(h*h + d*d) 65 | e = R1*sin(ang) 66 | alpha = atan(e/l) # half lower kite angle 67 | beta = pi - k_ang - alpha # half higher kite angle 68 | h2 = h*(tan(alpha)/tan(beta)+1) 69 | d1 = d*(tan(alpha)/tan(beta)+1) 70 | R2 = R0 - d1 71 | 72 | return R2, h0+h2 73 | 74 | 75 | def make_lamella_dome(pgon, radius_top, height_top, radius_bottom, 76 | height_bottom, kite_ang, first_level, last_level, caps): 77 | N = pgon.N 78 | a = pgon.angle()/2 79 | vals = [(radius_bottom, height_bottom), (radius_top, height_top)] 80 | 81 | if first_level > last_level: 82 | first_level, last_level = last_level, first_level 83 | 84 | max_above = last_level 85 | max_below = first_level 86 | 87 | for n in range(max_above): 88 | vals.append(get_next_radius_height(vals[-2], vals[-1], a, kite_ang)) 89 | if vals[-1][0] < 0: 90 | break 91 | 92 | vals2 = [vals[1], vals[0]] 93 | for n in range(-max_below): 94 | vals2.append(get_next_radius_height(vals2[-2], vals2[-1], a, kite_ang)) 95 | if vals2[-1][0] < 0: 96 | break 97 | 98 | all_vals = list(reversed(vals2[2:]))+vals 99 | if(max_below > 0): 100 | all_vals = all_vals[max_below:-1] 101 | if(max_above < 0): 102 | all_vals = all_vals[0:max_above] 103 | 104 | points = [] 105 | faces = [] 106 | for n in range(len(all_vals)): 107 | R1 = all_vals[n][0] 108 | h1 = all_vals[n][1] 109 | not_last = (n < len(all_vals)-2) 110 | 111 | a_off = a * (n % 2) 112 | for i in range(N): 113 | ang = a_off + i * 2 * pi / N 114 | pt = Vec(R1*cos(ang), R1*sin(ang), h1) 115 | points.append(pt) 116 | 117 | if not_last: 118 | faces.append([n*N+i, (n+1)*N+(i+N-(n+1) % 2) % N, 119 | (n+2)*N+i, (n+1)*N+(i+N+n % 2) % N]) 120 | 121 | for cap in range(2): 122 | last = len(all_vals) - 1 123 | if caps[cap] == 'a': 124 | apex_index = len(points) 125 | if(cap == 0): 126 | apex = get_axis_intersect(points[0], points[1], points[N]) 127 | else: 128 | apex = get_axis_intersect(points[last*N], points[last*N+1], 129 | points[(last-1)*N]) 130 | points.append(apex) 131 | for i in range(N): 132 | if cap == 0: 133 | faces.append([apex_index, i, N+i, (i+1) % N]) 134 | else: 135 | faces.append([(last-1)*N+(N + i - 1 + last % 2) % N, 136 | last*N+(N+i-1) % N, 137 | apex_index, last*N+i]) 138 | else: 139 | for i in range(N): 140 | if cap == 0: 141 | faces.append([i, N+i, (i+1) % N]) 142 | else: 143 | faces.append([(last-1)*N+(N + i - 1 + last % 2) % N, 144 | last*N+(N+i-1) % N, last*N+i]) 145 | faces.append([(last*cap)*N + i for i in range(N)]) 146 | 147 | return points, faces 148 | 149 | 150 | def main(): 151 | parser = argparse.ArgumentParser(description=__doc__) 152 | 153 | parser.add_argument( 154 | 'polygon_fraction', 155 | help='number of sides of the base polygon (N), ' 156 | 'or a fraction for star polygons (N/D) (default: 12)', 157 | default='12', 158 | nargs='?', 159 | type=anti_lib.read_polygon) 160 | parser.add_argument( 161 | 'last_level', 162 | help='number of levels above (+/-) to finish (default: 100,0)', 163 | nargs='?', 164 | type=int, 165 | default=100) 166 | parser.add_argument( 167 | 'first_level', 168 | help='number of levels above (+/-) to start (default: 0)', 169 | nargs='?', 170 | type=int, 171 | default=0) 172 | parser.add_argument( 173 | '-k', '--kite-angle', 174 | help='side angle of kite (default: calculated)', 175 | type=float, 176 | nargs='?', 177 | default=-1.0) 178 | parser.add_argument( 179 | '-l', '--height', 180 | help='height of base antiprism (default: calculated)', 181 | type=float, 182 | nargs='?', 183 | default=-1.0) 184 | parser.add_argument( 185 | '-a', '--alignment', 186 | help='alignment: p - prism, a - antiprism (default)', 187 | choices=['p', 'a'], 188 | default='a') 189 | parser.add_argument( 190 | '-c', '--caps', 191 | help='cap types for bottom and top (in that order), two letters ' 192 | 'from p (polygon), and a (apex) (default: pa)', 193 | choices=['aa', 'ap', 'pa', 'pp'], 194 | default='pa') 195 | parser.add_argument( 196 | '-o', '--outfile', 197 | help='output file name (default: standard output)', 198 | type=argparse.FileType('w'), 199 | default=sys.stdout) 200 | 201 | args = parser.parse_args() 202 | 203 | pgon = args.polygon_fraction 204 | if pgon.D > 1: 205 | parser.error('star polygons not currently implemented') 206 | pgon_a = pgon.angle() / 2 207 | 208 | radius_bottom = 1.0 209 | if args.alignment == 'a': 210 | radius_top = radius_bottom 211 | if args.height > 0: 212 | height_top = args.height / 2 213 | else: 214 | height_top = 0.5 * sin(pgon_a) * sqrt(2 - 1 / (cos(pgon_a / 2))**2) 215 | height_bottom = -height_top 216 | else: 217 | radius_top = radius_bottom * cos(pgon_a) 218 | if args.height > 0: 219 | height_top = args.height 220 | else: 221 | height_top = radius_bottom * sin(pgon_a) 222 | height_bottom = 0.0 223 | 224 | if args.kite_angle >= 0.0: 225 | ang = args.kite_angle*pi/180 226 | else: 227 | ang = get_default_angle(pgon, radius_top, radius_bottom, 228 | height_top-height_bottom) 229 | print("calculated angle:"+str(180/pi*ang), file=sys.stderr) 230 | 231 | points, faces = make_lamella_dome( 232 | pgon, radius_top, height_top, radius_bottom, height_bottom, 233 | ang, args.first_level, args.last_level, args.caps) 234 | 235 | out = anti_lib.OffFile(args.outfile) 236 | out.print_all_pgon(points, faces, pgon) 237 | 238 | if __name__ == "__main__": 239 | main() 240 | -------------------------------------------------------------------------------- /anti_lib_progs/lat_grid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Make a lattice or grid using integer coordinates. NOTE: if this program 24 | does not meet all your needs then also consider the Antiprism lat_grid 25 | and lat_util programs http://www.antiprism.com/programs/ 26 | ''' 27 | 28 | import sys 29 | import argparse 30 | import anti_lib 31 | from anti_lib import Vec 32 | 33 | epsilon = 1e-8 34 | 35 | 36 | def dist2_inside(v1, width): 37 | """Square of distance less than""" 38 | return (v1.mag2() < width+epsilon) 39 | 40 | 41 | def dist2_test(v1, v2, idx1, idx2, len2): 42 | """Square of distance equal""" 43 | return (v1-v2).mag2() == len2 44 | 45 | 46 | def sc_coord_test(x, y, z): # dist2 = 1, 2, 3 47 | """Test for coordinate in SC lattice""" 48 | return 1 49 | 50 | 51 | def fcc_coord_test(x, y, z): # dist2 = 2, 4, 6, 12 52 | """Test for coordinate in FCC lattice""" 53 | return (x+y+z) % 2 == 0 54 | 55 | 56 | def bcc_coord_test(x, y, z): # dist2 = 3, 4, 8 57 | """Test for coordinate in BCC lattice""" 58 | return (x % 2 == y % 2) and (y % 2 == z % 2) 59 | 60 | 61 | def rh_dodec_coord_test(x, y, z): # dist2 = 3 (8) 62 | """Test for coordinate in rhombic cuboctahedron grid""" 63 | return ((x % 2 + y % 2 + z % 2) == 0 or 64 | ((x % 2 + y % 2 + z % 2) == 3 and (x//2 + y//2) % 2 == (z//2) % 2)) 65 | 66 | 67 | def cubo_oct_coord_test(x, y, z): # dist2 = 2 68 | """Test for coordinate in octahedron/cuboctahedron grid""" 69 | return (x % 2 + y % 2 + z % 2) == 2 70 | 71 | 72 | def tr_oct_coord_test(x, y, z): # dist2 = 2 73 | """Test for coordinate in truncated octahedron grid""" 74 | return ((z % 4 == 0 and x % 4 and y % 4 and (x-y) % 2) or 75 | (y % 4 == 0 and z % 4 and x % 4 and (z-x) % 2) or 76 | (x % 4 == 0 and y % 4 and z % 4 and (y-z) % 2)) 77 | 78 | 79 | def tr_tet_tet_coord_test(x, y, z): # dist2 = 2 80 | """Test for coordinate in tretrahedron/truncated tetrahedron grid""" 81 | return ((x % 2 == 0 and y % 4 == ((x + z)) % 4) or 82 | (y % 2 == 0 and z % 4 == ((y + x)) % 4) or 83 | (z % 2 == 0 and x % 4 == ((z + y)) % 4)) 84 | 85 | 86 | def tr_tet_tr_oct_cubo_coord_test(x, y, z): # dist2 = 4 87 | """Test for coordinate in truncated tetrahedron/truncated octahedron/ 88 | cuboctahedron grid""" 89 | x = abs(x) % 6 90 | y = abs(y) % 6 91 | z = abs(z) % 6 92 | if x > 3: 93 | x = 6-x 94 | if y > 3: 95 | y = 6-y 96 | if z > 3: 97 | z = 6-z 98 | dist2 = x**2 + y**2 99 | return ((z % 6 == 0 and (dist2 == 2 or dist2 == 8)) or 100 | (z % 6 == 1 and (dist2 == 1 or dist2 == 13)) or 101 | (z % 6 == 2 and (dist2 == 4 or dist2 == 10)) or 102 | (z % 6 == 3 and dist2 == 5)) 103 | 104 | 105 | def diamond_coord_test(x, y, z): # dist2 = 3 106 | """Test for coordinate in diamond grid""" 107 | return (((x % 2 + y % 2 + z % 2) == 0 and (x//2+y//2+z//2) % 2 == 0) or 108 | ((x % 2 + y % 2 + z % 2) == 3 and (x//2+y//2+z//2) % 2 == 0)) 109 | 110 | 111 | def hcp2_coord_test(x, y, z): 112 | """Test for coordinate in HCP alternative grid""" 113 | return (x+y+z) % 6 == 0 and ((x-y) % 3 or (x-z) % 3) 114 | 115 | 116 | def hcp_coord_test(x, y, z): 117 | """Test for coordinate in HCP grid""" 118 | m = x+y+z 119 | return m % 6 == 0 and (x-m//12) % 3 == 0 and (y-m//12) % 3 == 0 120 | 121 | 122 | # Coordinates from Wendy Krieger 123 | def hcp_diamond_coord_test(x, y, z): 124 | """Test for coordinate in HCP diamond grid""" 125 | for pt in [[0, 0, 0], [3, 3, 3], [6, 0, 6], [9, 3, 9]]: 126 | tx = x - pt[0] 127 | ty = y - pt[1] 128 | tz = z - pt[2] 129 | s = tx + ty + tz 130 | if(s % 24 == 0): 131 | n8 = s//3 132 | if (tx-n8) % 6 == 0 and (ty-n8) % 6 == 0 and (tz-n8) % 6 == 0: 133 | return True 134 | return False 135 | 136 | 137 | def make_verts(lat_type, width, container_is_cube): 138 | """Generate coordinates""" 139 | verts = [] 140 | if container_is_cube: 141 | coords_start = 0 142 | else: # sphere 143 | coords_start = -width 144 | coords_end = width 145 | 146 | coord_test = eval(lat_type+"_coord_test") 147 | for x in range(coords_start, coords_end+1): 148 | for y in range(coords_start, coords_end+1): 149 | for z in range(coords_start, coords_end+1): 150 | if container_is_cube or dist2_inside(Vec(x, y, z), width): 151 | if coord_test(x, y, z): 152 | verts.append(Vec(x, y, z)) 153 | return verts 154 | 155 | 156 | def make_edges(verts, strut_len_sqd): 157 | """Add edges""" 158 | edges = [] 159 | if strut_len_sqd: 160 | for i in range(len(verts)-1): 161 | for j in range(i+1, len(verts)): 162 | if dist2_test(verts[i], verts[j], i, j, strut_len_sqd): 163 | edges.append((i, j)) 164 | return edges 165 | 166 | 167 | def main(): 168 | """Entry point""" 169 | epilog = ''' 170 | notes: 171 | Depends on anti_lib.py. 172 | 173 | examples: 174 | Oct-tet truss, space-frame 175 | lat_grid.py fcc 5 2 | antiview 176 | 177 | Spherical section of diamond 178 | lat_grid.py -s diamond 20 3 | antiview 179 | 180 | ''' 181 | 182 | parser = argparse.ArgumentParser(formatter_class=anti_lib.DefFormatter, 183 | description=__doc__, epilog=epilog) 184 | 185 | parser.add_argument( 186 | 'lat_type', 187 | help=''']Type of lattice or grid (default: sc). The numbers in 188 | brackets are suitable strut arguments. 189 | 190 | sc - Simple Cubic (1, 2, 3) 191 | fcc - Face Centred Cubic (2, 4, 6, 12) 192 | bcc - Body Centred Cubic (3, 4, 8) 193 | hcp - Hexagonal Close Packing (18) 194 | rh_dodec - Rhombic Dodecahedra (3, 8) 195 | cubo_oct - Cuboctahedron / Octahedron (2) 196 | tr_oct - Truncated Octahedron (2) 197 | tr_tet_tet - Trunc Tetrahedron/Tetrahedron (2) 198 | tr_tet_tr_oct_cubo - Trunc Tet / Trunc Oct / Cuboct (4) 199 | diamond - Diamond (3) 200 | hcp_diamond - HCP Diamond (27)''', 201 | choices=['sc', 'fcc', 'bcc', 'hcp', 'rh_dodec', 'cubo_oct', 202 | 'tr_oct', 'tr_tet_tet', 'tr_tet_tr_oct_cubo', 203 | 'diamond', 'hcp_diamond'], 204 | nargs='?', 205 | default="sc") 206 | parser.add_argument( 207 | 'width', 208 | help='''width of container cube (default: 6), or radius 209 | of contianer sphere''', 210 | type=anti_lib.read_positive_int, 211 | nargs='?', 212 | default=6) 213 | parser.add_argument( 214 | 'strut_len_sqd', 215 | help='''add struts between pairs of vertices that are 216 | separated by the square root of this distance''', 217 | type=anti_lib.read_positive_int, 218 | nargs='?', 219 | default=0) 220 | parser.add_argument( 221 | '-s', '--sphere-container', 222 | help='''container is a sphere (radius 'width') rather than 223 | a cube''', 224 | action='store_true') 225 | parser.add_argument( 226 | '-o', '--outfile', 227 | help='output file name (default: standard output)', 228 | type=argparse.FileType('w'), 229 | default=sys.stdout) 230 | 231 | args = parser.parse_args() 232 | 233 | verts = make_verts(args.lat_type, args.width, not args.sphere_container) 234 | edges = make_edges(verts, args.strut_len_sqd) 235 | 236 | out = anti_lib.OffFile(args.outfile) 237 | out.print_all(verts, edges) 238 | 239 | if __name__ == "__main__": 240 | main() 241 | -------------------------------------------------------------------------------- /anti_lib_progs/njitterbug.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Create a jitterbug model for a general polygon base. The transformation 24 | includes, if constructible, models corresponding to the antiprism, 25 | snub-antiprisms and gyrobicupola for that base. The transformation is 26 | controlled by the angle of an equatorial edge. 27 | ''' 28 | 29 | import argparse 30 | import sys 31 | import math 32 | from math import cos, sin, tan, sqrt 33 | import anti_lib 34 | from anti_lib import Vec 35 | 36 | EPSILON = 1e-15 # limit for precision considerations 37 | 38 | 39 | def print_antiprism_angle(p_ang): 40 | """Print the angle corresponding to an antiprism""" 41 | edge = 1 # polygon edge length 42 | rad = edge/(2*sin(p_ang)) # circumradius of base polygon 43 | dist = 2*rad*sin(p_ang/2) 44 | msg = 'angle parameter(s) for antiprism: ' 45 | val = 1 - dist**2 46 | if val < -EPSILON: 47 | msg += 'antiprism is not constructible' 48 | else: 49 | if p_ang > math.pi/2 + EPSILON: 50 | msg += '-x ' 51 | if val < 0.0: 52 | val = 0.0 53 | ht = sqrt(val) 54 | side = 2*rad*sin(p_ang/2) 55 | msg += str(math.degrees(math.atan2(ht, side))) 56 | 57 | print(msg, file=sys.stderr) 58 | 59 | 60 | def get_principal_verts(p_ang, ang, cross_flag): 61 | """Calculate example vertex coordinates""" 62 | edge = 1 # polygon edge length 63 | rad = edge/(2*sin(p_ang)) # circumradius of base polygon 64 | belt_irad = edge*cos(ang)/(2*tan(p_ang/2)) # inradius of belt polygon 65 | tri_ht = edge * sqrt(3)/2 66 | 67 | # Cut polygon cylinder (radius rad) by plane normal to a belt edge 68 | # and the through its centre point 69 | # Distance to this ellipse should be tri_ht 70 | S2 = sin(ang)**2 71 | if abs(S2) < EPSILON: 72 | x = (1-2*cross_flag)*rad 73 | 74 | val = tri_ht**2 - (belt_irad - x)**2 75 | if val < -EPSILON: 76 | raise ValueError('triangle cannot reach base polygon ' 77 | '(2nd coordinate)') 78 | elif val < 0.0: 79 | val = 0.0 80 | 81 | y = sqrt(val) 82 | 83 | else: 84 | a = (1-S2) 85 | b = 2*belt_irad*S2 86 | c = S2*tri_ht**2 - rad**2 - S2*belt_irad**2 87 | 88 | disc = b**2 - 4*a*c 89 | if disc < - EPSILON: 90 | raise ValueError('triangle cannot reach base polygon ' 91 | '(1st coordinate)') 92 | elif disc < 0.0: 93 | disc = 0.0 94 | 95 | if not a: 96 | raise ValueError('triangle cannot reach base polygon ' 97 | '(vertical edges)') 98 | 99 | x = (-b + (1-2*cross_flag)*sqrt(disc))/(2*a) 100 | 101 | val = (rad**2 - x**2)/S2 102 | if val < -EPSILON: 103 | raise ValueError('triangle cannot reach base polygon ' 104 | '(2nd coordinate)') 105 | elif val < 0.0: 106 | val = 0.0 107 | 108 | y = sqrt(val) 109 | 110 | return (Vec(x, y*sin(ang), y*cos(ang)), # base 111 | Vec(belt_irad, 0.5*edge*cos(ang), 0.5*edge*sin(ang))) # belt 112 | 113 | 114 | def make_jitterbug(pgon, ang, cross, right_fill, left_fill, delete_main_faces): 115 | """Make the jitterbug model""" 116 | p_ang = pgon.angle()/2 117 | A, B = get_principal_verts(p_ang, ang, cross) 118 | 119 | A2 = Vec(A[0], -A[1], -A[2]).rot_z(-p_ang) 120 | A = Vec(A2[0], A2[1], -A2[2]).rot_z(p_ang) 121 | B2 = Vec(B[0], -B[1], -B[2]) 122 | 123 | N = pgon.N 124 | points = [] 125 | for pt in [A, A2, B2, B]: 126 | points += [pt.rot_z(i*2*p_ang) for i in range(N)] 127 | 128 | faces = [] 129 | if not delete_main_faces: 130 | faces.append([i for i in range(N)]) # top 131 | faces.append([2*N-1 - i for i in range(N)]) # bottom 132 | 133 | # side triangles 134 | for i in range(N): 135 | faces.append([2*N+i, 3*N+i, (i+N) % N]) 136 | faces.append([3*N+i, 2*N+(i+1) % N, N + (i+1+N) % N]) 137 | 138 | # fill holes 139 | # Top hole: i, (i+1) % N, 2*N+(i+1) % N, 3*N+i 140 | # Bottom hole: N+i, N+(i-1+N) % N, 2*N+(i-1+N) % N, 3*N+(i-1) % N 141 | if right_fill: 142 | for i in range(N): 143 | faces.append([(i+1) % N, i, 3*N+i]) 144 | faces.append([3*N+i, (i+1) % N, 2*N+(i+1) % N]) 145 | faces.append([N+(i-1+N) % N, N+i, 2*N+(i-1+N) % N]) 146 | faces.append([2*N+(i-1+N) % N, N+i, 3*N+(i-1) % N]) 147 | if left_fill: 148 | for i in range(N): 149 | faces.append([i, (i+1) % N, 2*N+(i+1) % N]) 150 | faces.append([2*N+(i+1) % N, 3*N+i, i]) 151 | faces.append([N+i, N+(i-1+N) % N, 3*N+(i-1) % N]) 152 | faces.append([3*N+(i-1) % N, 2*N+(i-1+N) % N, N+(i-1+N) % N]) 153 | 154 | return points, faces 155 | 156 | 157 | def main(): 158 | """Entry point""" 159 | parser = argparse.ArgumentParser(description=__doc__) 160 | 161 | parser.add_argument( 162 | 'polygon_fraction', 163 | help='number of sides of the base polygon (N), ' 164 | 'or a fraction for star polygons (N/D)', 165 | default='3', 166 | type=anti_lib.read_polygon) 167 | parser.add_argument( 168 | 'angle', 169 | help='angle to rotate a belt edge from horizontal', 170 | type=float, 171 | default=0.0) 172 | parser.add_argument( 173 | '-x', '--cross', 174 | help='alternative model type that will make a ' 175 | 'cross-antiprism for fractions greater than 1/2', 176 | action='store_true') 177 | parser.add_argument( 178 | '-l', '--left-fill', 179 | help='fill holes with triangles using the "right" ' 180 | 'diagonal', 181 | action='store_true') 182 | parser.add_argument( 183 | '-r', '--right-fill', 184 | help='fill holes with triangles using the "left" ' 185 | 'diagonal', 186 | action='store_true') 187 | parser.add_argument( 188 | '-d', '--delete-main-faces', 189 | help='delete base polygons and side triangles, ' 190 | 'keep all vertices and any faces specified with -l, -r', 191 | action='store_true') 192 | parser.add_argument( 193 | '-p', '--print-info', 194 | help='print information about the model to the screen ' 195 | '(currently only parameters to make antiprism)', 196 | action='store_true') 197 | parser.add_argument( 198 | '-o', '--outfile', 199 | help='output file name (default: standard output)', 200 | type=argparse.FileType('w'), 201 | default=sys.stdout) 202 | 203 | args = parser.parse_args() 204 | 205 | pgon = args.polygon_fraction 206 | 207 | if args.print_info: 208 | print_antiprism_angle(pgon.angle()/2) 209 | 210 | try: 211 | points, faces = make_jitterbug( 212 | pgon, math.radians(args.angle), args.cross, args.right_fill, 213 | args.left_fill, args.delete_main_faces) 214 | except ValueError as e: 215 | parser.error(e.args[0]) 216 | 217 | out = anti_lib.OffFile(args.outfile) 218 | out.print_all_pgon(points, faces, pgon) 219 | 220 | if __name__ == "__main__": 221 | main() 222 | -------------------------------------------------------------------------------- /anti_lib_progs/njohnson.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Create a Johnson-based model, from J88, J89, J90, with a general polygon base. 24 | ''' 25 | 26 | import argparse 27 | import sys 28 | import math 29 | from math import cos, sin, tan, acos, atan2, sqrt 30 | import anti_lib 31 | from anti_lib import Vec, Mat 32 | 33 | epsilon = 1e-15 34 | edge = 1 35 | 36 | 37 | def get_three_line_vert(A0, A1, B0, B1, ridge_down): 38 | A_edge = A1 - A0 39 | B_edge = B0 - A0 40 | cos_gamma = Vec.dot(A_edge.unit(), B_edge.unit()) 41 | if abs(cos_gamma) > 1: 42 | cos_gamma /= abs(cos_gamma) 43 | gamma = acos(cos_gamma) 44 | d = (edge/2)*tan(gamma/2) 45 | h2 = 3*edge/4 - d**2 46 | if h2 < -epsilon: 47 | raise ValueError( 48 | 'Not possible to calculate a three-line-fill triangle') 49 | h = (1 - 2*ridge_down) * sqrt(h2) 50 | 51 | A_mid = (A0 + A1) / 2 52 | B_mid = (B0 + B1) / 2 53 | mid_dir = (B_mid - A_mid).unit() 54 | norm = Vec.cross(B_edge, A_edge).unit() 55 | P_from_A_mid = mid_dir*d + norm*h 56 | return A_mid + P_from_A_mid 57 | 58 | 59 | def get_pstt_cap_verts(pgon, ang, ridge_down): 60 | p_ang = pgon.angle()/2 61 | rad = pgon.circumradius(edge) 62 | 63 | # Polygon vertex 64 | A = Vec(rad, 0, 0) 65 | 66 | # First and last square vertices 67 | B = Vec(rad + edge*cos(ang)*cos(p_ang), 68 | edge*cos(ang)*sin(p_ang), 69 | edge*sin(ang)) 70 | 71 | B2 = Vec(B[0], -B[1], B[2]).rot_z(2*p_ang) 72 | 73 | A_minus1 = A.rot_z(-2*p_ang) 74 | B2_minus1 = B2.rot_z(-4*p_ang) 75 | C = get_three_line_vert(A, A_minus1, B, B2_minus1, ridge_down) 76 | 77 | return A, B, B2, C 78 | 79 | 80 | def get_pstts_cap_verts(pgon, ang, ridge_down): 81 | p_ang = pgon.angle()/2 82 | rad = pgon.circumradius(edge) 83 | 84 | # Polygon vertex 85 | A = Vec(rad, 0, 0) 86 | 87 | # First and last square vertices 88 | B = Vec(rad + edge*cos(ang)*cos(p_ang), 89 | edge*cos(ang)*sin(p_ang), 90 | edge*sin(ang)) 91 | 92 | B2 = Vec(B[0], -B[1], B[2]) 93 | 94 | # Triangle vertex 95 | C = Vec(0, 0, 0) 96 | mid = Vec(B[0], 0, B[2]) # M 97 | s1 = Vec(B[0]-rad, 0, B[2]).mag() # length from A to mid of B, B2 98 | 99 | # print("C = %g, sqrt(3)/(2*sin(alpha)) = %g\n" % 100 | # (cos(ang), sqrt(3/4)/sin(p_ang)), file=sys.stderr) 101 | 102 | # b is angle CAM 103 | cos_b = edge/(2*s1) # triangle pair is part of a disphenoid 104 | if abs(cos_b) > 1+epsilon: 105 | raise ValueError('Not possible to calculate ridge vertex') 106 | elif abs(cos_b) >= 1: 107 | b = 0 108 | else: 109 | b = acos(cos_b) 110 | 111 | mid_ang = atan2(mid[2], mid[0]-rad) 112 | 113 | if ridge_down: 114 | C_ang = -b + mid_ang # down 115 | else: 116 | C_ang = b + mid_ang # up 117 | 118 | C = Vec(rad + edge*cos(C_ang), 0, edge*sin(C_ang)) 119 | return A, B, B2, C 120 | 121 | 122 | def j88_get_principal_verts(pgon, ang, flags): 123 | bad = flags.strip('AB') 124 | if bad: 125 | raise ValueError('Unrecognised form flag(s) \''+bad+'\'') 126 | ridge = 'A' not in flags 127 | belt_back = 'B' in flags 128 | A, B, B2, C = get_pstts_cap_verts(pgon, ang, ridge) 129 | 130 | p_ang = pgon.angle()/2 131 | sq_mid = (B + B2.rot_z(2*p_ang)) / 2 132 | sq_mid_rad = Vec(sq_mid[0], sq_mid[1], 0).mag() 133 | A_rad = A[0] 134 | tri_ht2 = 3*edge/4 - (A_rad - sq_mid_rad)**2 135 | if tri_ht2 < -epsilon: 136 | raise ValueError( 137 | 'Not possible to calculate upper equilateral triangle') 138 | 139 | A2_ht = (1 - 2*belt_back)*sqrt(tri_ht2) + B[2] 140 | A2 = Vec(A_rad, 0, A2_ht).rot_z(p_ang) 141 | 142 | mid_B_B1 = Vec(B[0], 0, B[2]) 143 | ax = C - mid_B_B1 144 | rot = Mat.rot_axis_ang(ax, math.pi) 145 | pt = A - mid_B_B1 146 | C2 = rot * pt + mid_B_B1 147 | 148 | return A, B, B2, C, A2, C2 149 | 150 | 151 | def j88_make_poly(pgon, ang, flags): 152 | principal_pts = j88_get_principal_verts(pgon, ang, flags) 153 | N = pgon.N 154 | p_ang = pgon.angle()/2 155 | points = [] 156 | for pt in principal_pts: 157 | points += [pt.rot_z(i*2*p_ang) for i in range(N)] 158 | 159 | faces = [] 160 | # base polygons 161 | faces.append([i for i in range(N)]) 162 | faces.append([4*N + i for i in range(N)]) 163 | 164 | # squares 165 | for i in range(N): 166 | faces.append([i, N + i, 2*N + (i + 1) % N, (i + 1) % N]) 167 | 168 | # side triangles 169 | for i in range(N): 170 | faces.append([i, 3*N + i, N + i]) 171 | faces.append([i, 2*N + i, 3*N + i]) 172 | 173 | # vertical mirror triangles 174 | for i in range(N): 175 | faces.append([4*N + i, N + i, 2*N + (i+1) % N]) 176 | 177 | for i in range(N): 178 | # pentagon to form pyramid with C2 179 | pent = [4*N + i, N + i, 3*N + i, 2*N + i, 4*N + (i+N-1) % N] 180 | for p in range(5): 181 | faces.append([5*N+i, pent[p], pent[(p+1) % 5]]) 182 | 183 | return points, faces 184 | 185 | 186 | def j89_get_principal_verts(pgon, ang, flags): 187 | bad = flags.strip('ABC') 188 | if bad: 189 | raise ValueError('Unrecognised form flag(s) \''+bad+'\'') 190 | ridge_down = 'A' in flags 191 | ridge2_down = 'B' in flags 192 | belt_back = 'C' in flags 193 | pgon.N *= 2 194 | # p_ang2 = pgon2.angle() 195 | A, B, B2, C = get_pstt_cap_verts(pgon, ang, ridge_down) 196 | pgon.N //= 2 197 | 198 | p_ang = pgon.angle()/2 199 | A2_rad = pgon.circumradius(edge) # circumradius of top polygon 200 | sq_mid = (B + B2) / 2 201 | sq_mid_rad = Vec(sq_mid[0], sq_mid[1], 0).mag() 202 | tri_ht2 = 3/4 - (A2_rad - sq_mid_rad)**2 203 | if tri_ht2 < -epsilon: 204 | raise ValueError( 205 | 'Not possible to calculate upper equilateral triangle') 206 | 207 | A2_ht = (1 - 2*belt_back)*sqrt(tri_ht2) + B[2] 208 | A2 = Vec(A2_rad, 0, A2_ht).rot_z(p_ang/2) 209 | 210 | A2_minus1 = A2.rot_z(-2*p_ang) 211 | B2_minus1 = B2.rot_z(-2*p_ang) 212 | C2 = get_three_line_vert(A2_minus1, A2, B2_minus1, B, ridge2_down) 213 | return [pt.rot_z(p_ang/2) for pt in [A, B, B2, C, A2, C2]] 214 | 215 | 216 | def j89_make_poly(pgon, ang, flags): 217 | N = pgon.N 218 | p_ang = pgon.angle()/4 219 | principal_pts = j89_get_principal_verts(pgon, ang, flags) 220 | points = [principal_pts[0].rot_z(i*2*p_ang) for i in range(2*N)] 221 | for pt in principal_pts[1:]: 222 | points += [pt.rot_z(i*4*p_ang) for i in range(N)] 223 | 224 | faces = [] 225 | # base polygons 226 | faces.append([i for i in range(2*N)]) 227 | faces.append([5*N + i for i in range(N)]) 228 | 229 | # squares 230 | for i in range(N): 231 | faces.append([2*i, 2*N + i, 3*N + i, (2*i+1) % (2*N)]) 232 | 233 | # vertical mirror triangles above squares 234 | for i in range(N): 235 | faces.append([2*N + i, 3*N + i, 5*N + i]) 236 | 237 | # bottom vertical mirror triangles 238 | for i in range(N): 239 | faces.append([2*i, (2*i-1) % (2*N), 4*N + i]) 240 | 241 | # bottom slant triangles 242 | for i in range(N): 243 | faces.append([(2*i-1) % (2*N), 3*N + (i-1) % N, 4*N + i]) 244 | faces.append([2*i, 4*N + i, 2*N + i]) 245 | 246 | # top vertical mirror triangles 247 | for i in range(N): 248 | faces.append([5*N + (i-1) % N, 5*N + i, 6*N + i]) 249 | 250 | # top slant triangles 251 | for i in range(N): 252 | faces.append([5*N + i, 2*N + i, 6*N + i]) 253 | faces.append([5*N + (i-1) % N, 6*N + i, 3*N + (i-1) % N]) 254 | 255 | # mid slant triangles 256 | for i in range(N): 257 | faces.append([6*N + i, 2*N + i, 4*N + i]) 258 | faces.append([3*N + (i-1) % N, 6*N + i, 4*N + i]) 259 | 260 | return points, faces 261 | 262 | 263 | def j90_get_principal_verts(pgon, ang, flags): 264 | bad = flags.strip('AB') 265 | if bad: 266 | raise ValueError('Unrecognised form flag(s) \''+bad+'\'') 267 | ridge = 'A' not in flags 268 | belt_back = 'B' in flags 269 | A, B, B2, C = get_pstts_cap_verts(pgon, ang, ridge) 270 | 271 | p_ang = pgon.angle()/2 272 | 273 | # print("sin(mid_ang=%g (%g), cos(mid_ang)=%g(%g)" 274 | # % (sin(mid_ang), sqrt((1-cos(ang)**2)/(1-(sin(p_ang)*cos(ang))**2)), 275 | # cos(mid_ang), 276 | # sqrt((cos(p_ang)*cos(ang))**2/(1-(sin(p_ang)*cos(ang))**2))), 277 | # file=sys.stderr) 278 | 279 | # find height of equatorial triangle on vertical mirror plane 280 | sq_mid = (B + B2.rot_z(2*p_ang)) / 2 281 | # z_diff_b = vec_len([C[0], C[1], 0]) - vec_len([sq_mid[0], sq_mid[1], 0]) 282 | z_diff = (C - sq_mid.rot_z(-p_ang))[0] 283 | # print("z_diff:\n%0.17g\n%0.17g\n" % (z_diff, z_diff_b), file=sys.stderr) 284 | tri_ht2 = edge**2*3/4 - z_diff**2 285 | if tri_ht2 < -epsilon: 286 | raise ValueError('Not possible to calculate equatorial triangle') 287 | elif tri_ht2 <= 0: 288 | tri_ht = 0 289 | else: 290 | tri_ht = sqrt(tri_ht2) 291 | 292 | if(belt_back): 293 | tri_ht *= -1 294 | 295 | half_ht = (B[2] + C[2] + tri_ht)/2 296 | A[2] -= half_ht 297 | B[2] -= half_ht 298 | B2[2] -= half_ht 299 | C[2] -= half_ht 300 | 301 | return A, B, B2, C 302 | 303 | 304 | def j90_make_poly(pgon, ang, flags): 305 | principal_pts = j90_get_principal_verts(pgon, ang, flags) 306 | N = pgon.N 307 | p_ang = pgon.angle()/2 308 | points = [] 309 | for s in [1, -1]: 310 | for pt in principal_pts: 311 | pt[2] *= s 312 | points += [pt.rot_z(i*2*p_ang + s*p_ang/2) for i in range(N)] 313 | 314 | faces = [] 315 | # base polygons 316 | faces.append([i for i in range(N)]) 317 | faces.append([4*N + i for i in range(N)]) 318 | 319 | # squares 320 | for i in range(N): 321 | faces.append([i, N + i, 2*N + (i + 1) % N, (i + 1) % N]) 322 | faces.append([4*N + i, 4*N + N + i, 4*N + 2*N + (i + 1) % N, 323 | 4*N + (i + 1) % N]) 324 | 325 | # side triangles 326 | for i in range(N): 327 | faces.append([i, 3*N + i, N + i]) 328 | faces.append([4*N + i, 4*N + 3*N + i, 4*N + N + i]) 329 | faces.append([i, 2*N + i, 3*N + i]) 330 | faces.append([4*N + i, 4*N + 2*N + i, 4*N + 3*N + i]) 331 | 332 | # mirror triangles 333 | for i in range(N): 334 | faces.append([3*N + i, 5*N + i, 6*N + (i + 1) % N]) 335 | faces.append([7*N + i, N + (i-1+N) % N, 2*N + i]) 336 | 337 | for i in range(N): 338 | faces.append([N + i, 3*N + i, 6*N + (i + 1) % N]) 339 | faces.append([N + i, 6*N + (i + 1) % N, 7*N + (i + 1) % N]) 340 | faces.append([2*N + i, 7*N + i, 5*N + i]) 341 | faces.append([2*N + i, 5*N + i, 3*N + i]) 342 | 343 | return points, faces 344 | 345 | 346 | def print_increments(j_no, pgon, ang, ang_end, steps, flags, **kwargs): 347 | p_ang = pgon.angle()/2 348 | ang_inc = (ang_end - ang)/steps 349 | for i in range(steps): 350 | a = ang+i*ang_inc 351 | try: 352 | if j_no == '88': 353 | principal_pts = j88_get_principal_verts(pgon, a, flags) 354 | edge_len = (principal_pts[5] - principal_pts[4]).mag() 355 | elif j_no == '89': 356 | principal_pts = j89_get_principal_verts(pgon, a, flags) 357 | edge_len = (principal_pts[5] - principal_pts[3]).mag() 358 | elif j_no == '90': 359 | principal_pts = j90_get_principal_verts(pgon, a, flags) 360 | B_rot = principal_pts[1].rot_z(-p_ang/2) 361 | edge_len = Vec(0, 2*B_rot[1], 2*B_rot[2]).mag() 362 | except: 363 | edge_len = -0.001 364 | print("%-20.16g,%.16g" % (a*180/math.pi, edge_len), **kwargs) 365 | 366 | 367 | def main(): 368 | """Entry point""" 369 | parser = argparse.ArgumentParser(description=__doc__) 370 | 371 | parser.add_argument( 372 | 'johnson_no', 373 | help='johnson solid number to make the general form of', 374 | choices=['88', '89', '90'], 375 | nargs='?', 376 | default='90') 377 | parser.add_argument( 378 | 'polygon_fraction', 379 | help='number of sides of the base polygon (N), ' 380 | 'or a fraction for star polygons (N/D)', 381 | type=anti_lib.read_polygon, 382 | nargs='?', 383 | default='3') 384 | parser.add_argument( 385 | 'angle', 386 | help='angle to rotate a belt edge from horizontal', 387 | type=float, 388 | nargs='?', 389 | default=0.0) 390 | parser.add_argument( 391 | 'angle_end', 392 | help='if given, do not output the model, but instead ' 393 | 'print the free edge length for a range of models ' 394 | 'between angle and angle_end. This may be used to ' 395 | 'search for, and zero in on, unit edged models.', 396 | type=float, 397 | nargs='?', 398 | default=None) 399 | parser.add_argument( 400 | 'steps', 401 | help='number of steps in the range of models when ' 402 | 'printing the free edge lengths', 403 | type=anti_lib.read_positive_int, 404 | nargs='?', 405 | default=20) 406 | parser.add_argument( 407 | '-p', '--print-edge', 408 | help='print the free edge length to the screen when the ' 409 | 'output type is model (otherwise ignore)', 410 | action='store_true') 411 | parser.add_argument( 412 | '-f', '--form-flags', 413 | help='letters from A, B, C, ... which select an ' 414 | 'alternative form at the stages of the calculation ' 415 | 'where a choice exists. These will generally "pop" ' 416 | 'a vertex type in or outfile. For each model the ' 417 | 'available letters and an example vertex popped ' 418 | 'are: 88: A-3N, B-4N; 89: A-4N, B-6N, C-5N; ' 419 | '90: A-3N, B-N.', 420 | default='') 421 | parser.add_argument( 422 | '-o', '--outfile', 423 | help='output file name (default: standard output)', 424 | type=argparse.FileType('w'), 425 | default=sys.stdout) 426 | 427 | args = parser.parse_args() 428 | 429 | ang = math.radians(args.angle) 430 | pgon = args.polygon_fraction 431 | flags = args.form_flags 432 | 433 | try: 434 | if args.angle_end is None: 435 | if args.johnson_no == '88': 436 | (points, faces) = j88_make_poly(pgon, ang, flags) 437 | edge_len = (points[5*pgon.N] - points[4*pgon.N]).mag() 438 | elif args.johnson_no == '89': 439 | (points, faces) = j89_make_poly(pgon, ang, flags) 440 | edge_len = (points[6*pgon.N] - points[4*pgon.N]).mag() 441 | elif args.johnson_no == '90': 442 | (points, faces) = j90_make_poly(pgon, ang, flags) 443 | B_rot = points[pgon.N].rot_z(-pgon.angle()/2) 444 | edge_len = Vec(0, 2*B_rot[1], 2*B_rot[2]).mag() 445 | if(args.print_edge): 446 | print("free edge length:", edge_len, file=sys.stderr) 447 | out = anti_lib.OffFile(args.outfile) 448 | out.print_all_pgon(points, faces, pgon) 449 | else: 450 | end_ang = math.radians(args.angle_end) 451 | print_increments(args.johnson_no, pgon, ang, end_ang, args.steps, 452 | flags, file=args.outfile) 453 | 454 | except ValueError as e: 455 | parser.error(e.args[0]) 456 | 457 | if __name__ == "__main__": 458 | main() 459 | -------------------------------------------------------------------------------- /anti_lib_progs/packer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2003-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Pack balls in a sphere. The pack is seeded with two or more balls, then 24 | subsequent balls are added one at a time in three point contact in 25 | positions chosen by the packing method. 26 | ''' 27 | 28 | import argparse 29 | import math 30 | import sys 31 | from functools import cmp_to_key 32 | import anti_lib 33 | from anti_lib import Vec 34 | 35 | 36 | # return the coordinate and radius of the circle produced by the 37 | # intersection of two spheres with coors p1, p2 and radii r0, r1 38 | def sphere_intersection(p0, r0, p1, r1): 39 | p0_p1 = p1 - p0 40 | len_p0_p1 = p0_p1.mag() 41 | # do points have the same coordinates? 42 | if len_p0_p1 == 0: 43 | return None, None 44 | 45 | # take a cross section through the spheres, and consider triangle 46 | # between sphere centres and one intersection point. compare areas by 47 | # herons law and base*ht/2 to find ht (ht = R = circle radius) 48 | 49 | sp = (r0 + r1 + len_p0_p1) / 2.0 # semi perimeter 50 | 51 | # do spheres intersect? 52 | if len_p0_p1 > sp or r0 > sp or r1 > sp: 53 | return None, None 54 | 55 | area = math.sqrt(sp*(sp - r0)*(sp - r1)*(sp - len_p0_p1)) 56 | R = 2*area / len_p0_p1 57 | 58 | # find length of vec in direction p0 to p1 by cos / cos rule, hence coord 59 | P = p0 + p0_p1*(r0**2 + len_p0_p1**2 - r1**2)/(2*len_p0_p1**2) 60 | 61 | return P, R 62 | 63 | 64 | # return coordinates of the 2 (or less) spheres of radius R that touch 65 | # three spheres with coords p1, p2, p3 and radii r1, r2, r3 66 | def touching_spheres(R, p0, r0, p1, r1, p2, r2): 67 | # Add R to the radii, the intersection points of the three sheres are 68 | # the centres of the touching ball(s) 69 | r0 += R 70 | r1 += R 71 | r2 += R 72 | 73 | # find centre of circle nd readius where first two spheres intersect 74 | pc1, rc1 = sphere_intersection(p0, r0, p1, r1) 75 | if pc1 is None: 76 | return None, None 77 | 78 | # the second circle is defined by th plane of first circle intersecting 79 | # the third sphere 80 | # find centre of second circle 81 | p2_pc1 = pc1 - p2 82 | p0_p1 = p1 - p0 83 | unit_p0_p1 = p0_p1.unit() 84 | len_p2_pc2 = Vec.dot(p2_pc1, unit_p0_p1) 85 | 86 | # is circle outside of third sphere 87 | if not (r2 > len_p2_pc2 > -r2): 88 | return None, None 89 | 90 | p2_pc2 = unit_p0_p1 * len_p2_pc2 91 | pc2 = p2 + p2_pc2 92 | 93 | rc2 = math.sqrt(r2**2 - len_p2_pc2**2) 94 | 95 | # find pt - intersection of plane of sphere centres and line joining 96 | # touching spheres, and R distance pt to a touching sphere centre. 97 | pt, R = sphere_intersection(pc1, rc1, pc2, rc2) 98 | if pt is None: 99 | return None, None 100 | 101 | # make a vector length R perpendicular to the plane of the sphere centres 102 | R_norm = Vec.cross(p1 - p0, p2 - p0) 103 | len_R_norm = R_norm.mag() 104 | if not len_R_norm: 105 | return None, None 106 | 107 | R_norm = R_norm * R/len_R_norm 108 | 109 | return pt + R_norm, pt - R_norm 110 | 111 | # ------- End of Pocket functions ----------- 112 | 113 | epsilon = 1e-12 114 | 115 | 116 | # compare function to sort on distance to to centre 117 | def cmp_from_orig(a, b): 118 | a2 = a[0]**2 + a[1]**2 + a[2]**2 119 | b2 = b[0]**2 + b[1]**2 + b[2]**2 120 | if a2 < b2 - epsilon: 121 | return -1 122 | if a2 > b2 + epsilon: 123 | return 1 124 | return 0 125 | 126 | 127 | # compare function to sort on distance to to centre 128 | def cmp_from_orig_up(a, b): 129 | a2 = a[0]**2 + a[1]**2 + a[2]**2 130 | b2 = b[0]**2 + b[1]**2 + b[2]**2 131 | if a2 < b2 - epsilon: 132 | return -1 133 | if a2 > b2 + epsilon: 134 | return 1 135 | 136 | # a==b 137 | if a[2] < b[2] - epsilon: 138 | return -1 139 | if a[2] > b[2] + epsilon: 140 | return 1 141 | 142 | return 0 143 | 144 | 145 | # compare function to sort on distance from container 146 | def cmp_from_cont(a, b): 147 | a2 = a[0]**2 + a[1]**2 + a[2]**2 148 | b2 = b[0]**2 + b[1]**2 + b[2]**2 149 | if a2 > b2 + epsilon: 150 | return -1 151 | if a2 < b2 - epsilon: 152 | return 1 153 | return 0 154 | 155 | 156 | # compare function to sort on distance from container then z 157 | def cmp_from_cont_up(a, b): 158 | a2 = a[0]**2 + a[1]**2 + a[2]**2 159 | b2 = b[0]**2 + b[1]**2 + b[2]**2 160 | if a2 > b2 + epsilon: 161 | return -1 162 | if a2 < b2 - epsilon: 163 | return 1 164 | 165 | # a==b 166 | if a[2] < b[2] - epsilon: 167 | return -1 168 | if a[2] > b[2] + epsilon: 169 | return 1 170 | 171 | return 0 172 | 173 | 174 | # compare function to sort on z 175 | def cmp_z(a, b): 176 | if a[2] < b[2] - epsilon: 177 | return -1 178 | if a[2] > b[2] + epsilon: 179 | return 1 180 | return 0 181 | 182 | 183 | # choose next ball position from pocket list 184 | def next_ball_pos(points, pkts, r): 185 | for i in range(len(pkts)): 186 | # would a ball in this pocket overlap with any other ball 187 | for n in range(1, len(points)): 188 | overlap = 0 189 | v = pkts[i] - points[n] 190 | if v.mag2() < 4*r**2 - epsilon: 191 | overlap = 1 192 | break 193 | if not overlap: 194 | break 195 | 196 | if overlap: 197 | del pkts 198 | return None 199 | 200 | next_p = pkts[i] # first suitable pocket 201 | del pkts[0:i + 1] # remove this, and earlier unsuitable pockets 202 | return next_p 203 | 204 | 205 | def find_pockets(points, new_p, r, R): 206 | new_pkts = [] 207 | l = len(points) 208 | for i in range(l): 209 | # set radius of first ball (0 is the container ball) 210 | if i: 211 | ri = r 212 | else: 213 | ri = R - 2*r 214 | for j in range(i + 1, l): 215 | pt = touching_spheres(r, points[i], ri, points[j], r, new_p, r) 216 | if pt[0] is None: 217 | continue 218 | for p in pt: 219 | # check it is within container 220 | if p.mag2() < (R - r)**2 + epsilon: 221 | new_pkts.append(p) 222 | return new_pkts 223 | 224 | 225 | def positive_float(val_str): 226 | try: 227 | val = float(val_str) 228 | except: 229 | raise argparse.ArgumentTypeError('not a number') 230 | 231 | if val <= 0.0: 232 | raise argparse.ArgumentTypeError('not a positive number') 233 | 234 | return val 235 | 236 | 237 | def main(): 238 | """Entry point""" 239 | parser = argparse.ArgumentParser(description=__doc__) 240 | 241 | parser.add_argument( 242 | 'ball_radius', 243 | help='radius of balls to pack in container ' 244 | '(default: 0.3)', 245 | type=anti_lib.read_positive_float, 246 | nargs='?', 247 | default=0.3) 248 | parser.add_argument( 249 | 'container_radius', 250 | help='radius of container sphere (default: 1.0)', 251 | type=anti_lib.read_positive_float, 252 | nargs='?', 253 | default=1.0) 254 | parser.add_argument( 255 | '-m', '--method', 256 | help='packing method: up - bottom up (default), ' 257 | 'in - outside to centre, out - centre to outside, ' 258 | 'inup - outside in bottom first, ' 259 | 'outup - centre to outside bottom first.', 260 | choices=['up', 'in', 'out', 'inup', 'outup'], 261 | default='out') 262 | parser.add_argument( 263 | '-o', '--outfile', 264 | help='output file name (default: standard output)', 265 | type=argparse.FileType('w'), 266 | default=sys.stdout) 267 | 268 | args = parser.parse_args() 269 | 270 | r = args.ball_radius 271 | R = args.container_radius 272 | if r > R: 273 | parser.error('ball_radius is greater than container_radius\n') 274 | 275 | points = [] 276 | points.append(Vec(0, 0, 0)) # container sphere/ball 277 | 278 | if args.method == 'up': 279 | cmp_func = cmp_z 280 | points.append(Vec(0, 0, -(R - r))) # first ball at min z 281 | # dummy ball point to find starting pockets 282 | dummy_p = Vec(r, 0, -(R - r)) 283 | elif args.method == 'inup': 284 | cmp_func = cmp_from_cont_up 285 | points.append(Vec(0, 0, -(R - r))) # first ball at min z 286 | # dummy ball point to find starting pockets 287 | dummy_p = Vec(r, 0, -(R - r)) 288 | elif args.method == 'in': 289 | cmp_func = cmp_from_cont 290 | points.append(Vec(0, 0, -(R - r))) # first ball at min z 291 | # dummy ball point to find starting pockets 292 | dummy_p = Vec(r, 0, -(R - r)) 293 | elif args.method == 'outup': 294 | cmp_func = cmp_from_orig_up 295 | points.append(Vec(0, 0, 0)) # ball at centre 296 | points.append(Vec(0, 0, -2*r)) # ball below 297 | # dummy ball point to find starting pockets 298 | dummy_p = Vec(2*r, 0, 0) 299 | else: # args.method=="out" 300 | cmp_func = cmp_from_orig 301 | points.append(Vec(0, 0, 0)) # ball at centre 302 | points.append(Vec(0, 0, 2*r)) # ball above 303 | # dummy ball point to find starting pockets 304 | dummy_p = Vec(2*r, 0, 0) 305 | 306 | pkts = find_pockets(points, dummy_p, r, R) 307 | 308 | if pkts: 309 | while 1: 310 | new_p = next_ball_pos(points, pkts, r) 311 | if new_p is None: 312 | break 313 | 314 | new_pkts = find_pockets(points, new_p, r, R) 315 | if new_pkts: 316 | pkts.extend(new_pkts) 317 | pkts.sort(key=cmp_to_key(cmp_func)) 318 | points.append(new_p) 319 | 320 | print('packed {} balls of radius {} in container of radius {}\n'.format( 321 | len(points) - 1, r, R), file=sys.stderr) 322 | 323 | out = anti_lib.OffFile(args.outfile) 324 | out.print_all(points[1:], []) 325 | 326 | if __name__ == "__main__": 327 | main() 328 | -------------------------------------------------------------------------------- /anti_lib_progs/pentabelt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Make a model with axial symmetry based on a belt of staggered pentagons 24 | ''' 25 | 26 | import argparse 27 | import sys 28 | import math 29 | import anti_lib 30 | from anti_lib import Vec 31 | 32 | epsilon = 1e-13 33 | phi = (math.sqrt(5) + 1)/2 34 | 35 | 36 | def calculate_belt_points(pgon, model_type): 37 | ang = pgon.angle()/2 38 | tan_a = math.sin(ang)/(math.cos(ang)+phi) # half of unit edge 39 | P = Vec(1/(2*tan_a), -1/2, 0) 40 | tan_b = math.sin(ang)/(math.cos(ang)+1/phi) # half of phi edge 41 | Q = Vec(phi/(2*tan_b), -phi/2, 0) 42 | bar_ht = math.cos(math.pi/10) # planar height of pentagon "bar" 43 | diff_r = P[0] - Q[0] 44 | try: 45 | ht0 = math.sqrt(bar_ht**2 - diff_r**2)/2 46 | except: 47 | raise ValueError('model is not constructible') 48 | 49 | P[2] = -ht0 50 | Q[2] = ht0 51 | pent_ht = math.sqrt(5 + 2*math.sqrt(5))/2 # planar height of pentagon 52 | x_cap = P[0] - pent_ht*(diff_r/bar_ht) 53 | z_cap = -ht0 + pent_ht*((2*ht0)/bar_ht) 54 | R = Vec(x_cap, 0, z_cap) 55 | S = Vec(x_cap*math.cos(ang), x_cap*math.sin(ang), -z_cap) 56 | cap_inrad = x_cap*math.cos(ang) 57 | apex_ht = 0 58 | if model_type == 'a': 59 | try: 60 | apex_ht = z_cap + cap_inrad * (z_cap + P[2])/(P[0] - cap_inrad) 61 | except: 62 | raise ValueError('could not calculate apex height') 63 | A = Vec(0, 0, apex_ht) 64 | return [P, Q, R, S, A] 65 | 66 | 67 | def make_model(pgon, model_type): 68 | pts = calculate_belt_points(pgon, model_type) 69 | N = pgon.N 70 | ang = pgon.angle() 71 | points = [] 72 | for point in pts[0:2]: 73 | for i in range(N): 74 | points.append(point.rot_z(i*ang)) 75 | points.append((point - Vec(0, 2*point[1], 0)).rot_z(i*ang)) 76 | 77 | for point in pts[2:4]: 78 | points += [point.rot_z(i*ang) for i in range(N)] 79 | 80 | if model_type == 'a': 81 | points += [pts[4], -pts[4]] 82 | 83 | faces = [] 84 | if model_type == 't': 85 | for off in [4*N, 5*N]: 86 | faces += [[i+off for i in range(N)]] 87 | 88 | for i in range(N): 89 | faces += [[2*N + 2*i, 4*N + i, 2*N + 2*i+1, 2*i+1, 2*i]] 90 | faces += [[2*i+1, 5*N + i, (2*i+2) % (2*N), 91 | 2*N + (2*i+2) % (2*N), 2*N + 2*i+1]] 92 | if model_type == 't': 93 | faces += [[2*i, 2*i+1, 5*N + i, 5*N + (N+i-1) % N]] 94 | faces += [[2*N + 2*i+1, 2*N + (2*i+2) % (2*N), 95 | 4*N + (i+1) % N, 4*N + i]] 96 | else: 97 | faces += [[2*i, 2*i+1, 5*N + i, 6*N + 1, 5*N + (N+i-1) % N]] 98 | faces += [[2*N + 2*i+1, 2*N + (2*i+2) % (2*N), 99 | 4*N + (i+1) % N, 6*N, 4*N + i]] 100 | 101 | return points, faces 102 | 103 | 104 | def main(): 105 | """Entry point""" 106 | parser = argparse.ArgumentParser(description=__doc__) 107 | 108 | parser.add_argument( 109 | 'polygon', 110 | help='number of pairs of pentagons (default: 4) ' 111 | '(or may be a polygon fraction, e.g. 5/2)', 112 | type=anti_lib.read_polygon, 113 | nargs='?', 114 | default='7') 115 | parser.add_argument( 116 | '-m', '--model_type', 117 | help='model type: a - with apex, t - truncated apex', 118 | default='t') 119 | parser.add_argument( 120 | '-o', '--outfile', 121 | help='output file name (default: standard output)', 122 | type=argparse.FileType('w'), 123 | default=sys.stdout) 124 | 125 | args = parser.parse_args() 126 | 127 | pgon = args.polygon 128 | 129 | try: 130 | points, faces = make_model(pgon, args.model_type) 131 | except Exception as e: 132 | parser.error(e.args[0]) 133 | 134 | out = anti_lib.OffFile(args.outfile) 135 | out.print_all_pgon(points, faces, pgon) 136 | 137 | if __name__ == "__main__": 138 | main() 139 | -------------------------------------------------------------------------------- /anti_lib_progs/proj_dome.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Make a Jacoby-style dome, as described in 24 | http://www.google.com/patents/US7900405 . Project a tiling 25 | of unit-edged triangles, squares or crossed squares (unit edges), 26 | at a specified height, onto a unit hemisphere, by gnomonic, 27 | stereographic or general point projection 28 | ''' 29 | 30 | import argparse 31 | import sys 32 | import math 33 | import anti_lib 34 | from anti_lib import Vec, Mat 35 | 36 | epsilon = 1e-13 37 | 38 | 39 | def make_sq_tiling(freq): 40 | points = [] 41 | faces = [] 42 | grads = 2*freq+1 43 | for i in range(grads): 44 | for j in range(grads): 45 | points.append(Vec(i-freq, j-freq, 0)) 46 | if i < grads-1 and j < grads-1: 47 | faces.append([j + i*grads, j+1 + i*grads, 48 | j+1 + (i+1)*grads, j + (i+1)*grads]) 49 | return points, faces, points[0].mag() 50 | 51 | 52 | def make_sq_x_tiling(freq): 53 | points = [] 54 | faces = [] 55 | grads = 2*freq+1 56 | for i in range(grads): 57 | for j in range(grads): 58 | points.append(Vec(i-freq, j-freq, 0)) 59 | for i in range(grads-1): 60 | for j in range(grads-1): 61 | cent_idx = len(points) 62 | points.append(Vec(i-freq+0.5, j-freq+0.5, 0)) 63 | face = [j + i*grads, j+1 + i*grads, 64 | j+1 + (i+1)*grads, j + (i+1)*grads] 65 | for v in range(4): 66 | faces.append([face[v], face[(v+1) % 4], cent_idx]) 67 | 68 | return points, faces, points[0].mag() 69 | 70 | 71 | def get_tri_faces(t, freq): 72 | line0 = [t*freq+i+1 for i in range(freq)] 73 | line_adj = [((t+1) % 6)*freq+i+1 for i in range(freq)] 74 | inner_cnt = freq*(freq-1)//2 75 | lines = [[0]] 76 | n = 1 + freq*6 + t*inner_cnt 77 | for i in range(0, freq): 78 | line = [line0[i]] 79 | for j in range(0, i): 80 | line.append(n) 81 | n = n+1 82 | line.append(line_adj[i]) 83 | lines.append(line) 84 | faces = [] 85 | for i in range(freq): 86 | for j in range(i+1): 87 | if i != freq-1: 88 | faces.append([lines[i+1][j+1], lines[i+1][j], lines[i+2][j+1]]) 89 | faces.append([lines[i+1][j], lines[i+1][j+1], lines[i][j]]) 90 | 91 | return faces 92 | 93 | 94 | def make_hexagonal_tiling(freq): 95 | grads = freq + 1 96 | points = [[0, 0, 0]] 97 | faces = [] 98 | for i in range(6): 99 | rot = Mat.rot_axis_ang(Vec(0, 0, 1), i*math.pi/3) 100 | for j in range(1, grads): 101 | points.append(rot * Vec(j, 0, 0)) 102 | 103 | for t in range(6): 104 | p1 = ((t+1) % 6)*freq + 1 105 | p2 = t*freq + 1 106 | vec = points[p1] - points[p2] 107 | for i in range(1, freq): 108 | for j in range(0, i): 109 | points.append(points[t*freq+1+i] + vec*(j+1)) 110 | faces += get_tri_faces(t, freq) 111 | 112 | return points, faces, points[freq].mag() 113 | 114 | 115 | def project_onto_sphere(points, ht, proj_ht): 116 | new_points = [] 117 | for point in points: 118 | a = point[0] 119 | b = point[1] 120 | d = ht-proj_ht 121 | if abs(d) < epsilon: 122 | z = 1 123 | x = 0 124 | y = 0 125 | else: 126 | z = (a**2 + b**2 - d**2) / (a**2 + b**2 + d**2) 127 | x = a * (z-1) / d 128 | y = b * (z-1) / d 129 | 130 | new_points.append(Vec(x, y, -z)) 131 | return new_points 132 | 133 | 134 | def parallel_project(points, R, rad): 135 | new_points = [] 136 | for point in points: 137 | x = point[0]*rad/R 138 | y = point[1]*rad/R 139 | try: 140 | z = math.sqrt(1 - (x**2 + y**2)) 141 | except Exception: 142 | z = 0 143 | new_points.append(Vec(x, y, -z)) 144 | return new_points 145 | 146 | 147 | def main(): 148 | """Entry point""" 149 | parser = argparse.ArgumentParser(description=__doc__) 150 | 151 | parser.add_argument( 152 | 'frequency', 153 | help='frequency (default: 5)', 154 | type=anti_lib.read_positive_int, 155 | nargs='?', 156 | default=5) 157 | parser.add_argument( 158 | 'tiling_ht', 159 | help='tiling_height (default: 4)', 160 | type=float, 161 | nargs='?', 162 | default=4) 163 | parser.add_argument( 164 | '-p', '--projection-height', 165 | help='height of projection centre below sphere centre, ' 166 | '0.0 - gnomonic projection (default), ' 167 | '1.0 - stereographic projection, values with a ' 168 | 'magnitude greater than 1 may not be geometrically ' 169 | 'realisable if the tiling is wider than the sphere', 170 | type=float, 171 | default=0.0) 172 | parser.add_argument( 173 | '-t', '--tiling-type', 174 | help='type of tiling: t - triangles, s - squares, ' 175 | 'x - squares triangulated to centres', 176 | choices=['t', 's', 'x'], 177 | default='t') 178 | parser.add_argument( 179 | '-P', '--parallel-project-radius', 180 | help='set radius of tiling (<=1) and parallel project onto unit ' 181 | 'sphere', 182 | type=float) 183 | parser.add_argument( 184 | '-o', '--outfile', 185 | help='output file name (default: standard output)', 186 | type=argparse.FileType('w'), 187 | default=sys.stdout) 188 | 189 | args = parser.parse_args() 190 | 191 | if args.tiling_ht < epsilon: 192 | parser.error('tiling height cannot be very small, or negative ') 193 | args.tiling_ht = args.tiling_ht+1 194 | 195 | if args.tiling_type == 't': 196 | points, faces, R = make_hexagonal_tiling(args.frequency) 197 | elif args.tiling_type == 's': 198 | points, faces, R = make_sq_tiling(args.frequency) 199 | elif args.tiling_type == 'x': 200 | points, faces, R = make_sq_x_tiling(args.frequency) 201 | 202 | rad = args.parallel_project_radius 203 | print(rad, file=sys.stderr) 204 | if(rad is None): 205 | points = project_onto_sphere( 206 | points, args.tiling_ht, args.projection_height) 207 | elif(rad >= 0.0 and rad <= 1.0): 208 | points = parallel_project(points, R, rad) 209 | else: 210 | parser.error('option -P: radius must be between 0 and 1.0') 211 | 212 | out = anti_lib.OffFile(args.outfile) 213 | out.print_all(points, faces) 214 | 215 | if __name__ == "__main__": 216 | main() 217 | -------------------------------------------------------------------------------- /anti_lib_progs/ring_place.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Place maximum radius rings of contacting balls around points on a sphere. 24 | Input is a list of coordinates, one set per line. 25 | ''' 26 | 27 | import argparse 28 | import sys 29 | import math 30 | import re 31 | import anti_lib 32 | from anti_lib import Vec, Mat 33 | 34 | 35 | def ring_ball_ang(N, a): 36 | target = math.sin(math.pi/(2*N)) 37 | ang_range = [0, a/2] 38 | for i in range(100): 39 | diff = (ang_range[1] - ang_range[0])/2 40 | b = (ang_range[0] + ang_range[1])/2 41 | if math.sin(b/2)/math.sin(a-b) > target: 42 | ang_range[1] -= diff 43 | else: 44 | ang_range[0] += diff 45 | 46 | return b 47 | 48 | 49 | def make_ring(R, N, a): 50 | b = ring_ball_ang(N, a) 51 | P = Vec(R*math.sin(a-b), 0, R*math.cos(a-b)) 52 | return [P.rot_z(2*math.pi*i/N) for i in range(N)], b 53 | 54 | 55 | def read_coords(): 56 | points = [] 57 | while 1: 58 | line = sys.stdin.readline() 59 | if line == '\n': 60 | continue 61 | 62 | if line == "": 63 | break 64 | 65 | m = re.search('^ *([^ ,]+) *,? *([^ ,]+) *,? *([^ ,\n]+) *$', line) 66 | if not m: 67 | sys.stderr.write( 68 | 'error: did not find x, y and z values in following ' 69 | 'line (1):\n') 70 | sys.stderr.write(line) 71 | sys.exit(1) 72 | else: 73 | try: 74 | points.append(Vec(*[float(m.group(i)) for i in range(1, 3+1)])) 75 | except: 76 | sys.stderr.write( 77 | 'error: did not find x, y and z values in following ' 78 | 'linei (2):\n') 79 | sys.stderr.write(line) 80 | sys.exit(1) 81 | 82 | return points 83 | 84 | 85 | def find_minimum_separation(points): 86 | min_dist2 = 1e100 87 | for i in range(len(points)-1): 88 | for j in range(i+1, len(points)): 89 | v = points[i] - points[j] 90 | dist2 = v.mag2() 91 | if dist2 < min_dist2: 92 | min_dist2 = dist2 93 | 94 | return math.sqrt(min_dist2) 95 | 96 | 97 | def main(): 98 | """Entry point""" 99 | parser = argparse.ArgumentParser(description=__doc__) 100 | 101 | parser.add_argument( 102 | 'num_balls_on_ring', 103 | help='Number of balls on each ring', 104 | type=int, 105 | nargs='?', 106 | default=10) 107 | parser.add_argument( 108 | '-o', '--outfile', 109 | help='output file name (default: standard output)', 110 | type=argparse.FileType('w'), 111 | default=sys.stdout) 112 | 113 | args = parser.parse_args() 114 | 115 | ring_centres = read_coords() 116 | if not len(ring_centres): 117 | parser.error('no coordinates in input') 118 | 119 | R = ring_centres[0].mag() 120 | dist = find_minimum_separation(ring_centres) 121 | a = math.asin(dist/(2*R)) 122 | ball_points, ball_ang = make_ring(R, args.num_balls_on_ring, a) 123 | print('ball radius = %.14f' % (2*R*math.sin(ball_ang/2)), file=sys.stderr) 124 | 125 | out = anti_lib.OffFile(args.outfile) 126 | out.print_header(len(ring_centres)*len(ball_points), 0) 127 | for cent in ring_centres: 128 | mat = Mat.rot_from_to(Vec(0, 0, 1), cent) 129 | out.print_verts([mat * p for p in ball_points]) 130 | 131 | if __name__ == "__main__": 132 | main() 133 | -------------------------------------------------------------------------------- /anti_lib_progs/rotegrity_models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Create cyclic rotegrity models with 1 or 2 layers of units 24 | ''' 25 | import argparse 26 | import sys 27 | import math 28 | from math import cos, sin, tan, pi 29 | import anti_lib 30 | 31 | 32 | def make_model(pgon, frac, num_layers): 33 | """Make cyclic rotegrity model""" 34 | N = pgon.N 35 | a = pgon.angle() / 2 36 | if num_layers == 1: 37 | if math.isnan(frac): 38 | e_ang = 2*math.atan(tan(a)/math.sqrt(2)) 39 | else: 40 | e_ang = pi * frac / (1 - frac) 41 | else: 42 | e_ang = math.acos(2*cos(a)-1) 43 | 44 | sin_a = tan(e_ang/2)/tan(a) 45 | if sin_a != anti_lib.safe_for_trig(sin_a): 46 | return [], [], 0, 0 # not constructible 47 | 48 | rot = math.asin(sin_a) 49 | ax_ang = rot - pi/2 50 | mid_edge = anti_lib.Vec(sin(rot), 0, cos(rot)) 51 | axis = anti_lib.Vec(sin(ax_ang), 0, cos(ax_ang)) 52 | pt_angs = [-e_ang/2, e_ang/2, pi/num_layers-e_ang/2, pi/num_layers+e_ang/2] 53 | pts = [anti_lib.Mat.rot_axis_ang(axis, pt_ang) * mid_edge 54 | for pt_ang in pt_angs] 55 | if num_layers == 2: 56 | mid_ht = (pts[2][2]+pts[3][2])/2 57 | for i in range(4): 58 | pts[i][2] -= mid_ht 59 | 60 | points = [anti_lib.Vec(0, 0, 0)] * (2*num_layers) * N 61 | faces = [] 62 | for i in range(N): 63 | ang = i * 2 * a 64 | rot_i = anti_lib.Mat.rot_xyz(0, 0, ang) 65 | rot_i2 = anti_lib.Mat.rot_xyz(0, 0, ang + a) 66 | if num_layers == 1: 67 | points[i] = rot_i * pts[0] 68 | points[i+N] = rot_i * pts[2] 69 | faces.append([N + (i+1) % N, i, (i+1) % N, N + i]) 70 | else: # num_layers == 2: 71 | points[i] = rot_i * pts[0] 72 | points[i+N] = rot_i * pts[2] 73 | points[i+2*N] = rot_i * pts[3] 74 | points[i+3*N] = rot_i2 * pts[0] 75 | points[i+3*N][2] *= -1 76 | 77 | faces.append([i+2*N, i, (i+1) % N, i+N]) 78 | faces.append([(i+1) % N + N, i + 3*N, (i+1) % N + 3*N, i + 2*N]) 79 | 80 | return points, faces, e_ang, pi/num_layers + e_ang 81 | 82 | 83 | def main(): 84 | """Entry point""" 85 | epilog = ''' 86 | notes: 87 | Depends on anti_lib.py. 88 | 89 | examples: 90 | octagonal hosohedron rotegrity 91 | rotegrity_pretwist.py 8 | antiview 92 | ''' 93 | 94 | parser = argparse.ArgumentParser(formatter_class=anti_lib.DefFormatter, 95 | description=__doc__, epilog=epilog) 96 | 97 | parser.add_argument( 98 | 'polygon_fraction', 99 | help='number of sides of the base polygon (N), ' 100 | 'or a fraction for star polygons (N/D) (default: 6)', 101 | default='6', 102 | nargs='?', 103 | type=anti_lib.read_polygon) 104 | parser.add_argument( 105 | '-f', '--fraction', 106 | help='end fraction (default: calculated valid value), ' 107 | 'only used for 1-layer models', 108 | type=float, 109 | default=float('nan')) 110 | parser.add_argument( 111 | '-n', '--number-layers', 112 | help='number of layers (default: 1)', 113 | choices=['1', '2'], 114 | default='1') 115 | parser.add_argument( 116 | '-c', '--curved-struts', 117 | help='represent struts by a curved sequence of edges', 118 | action='store_true') 119 | parser.add_argument( 120 | '-o', '--outfile', 121 | help='output file name (default: standard output)', 122 | type=argparse.FileType('w'), 123 | default=sys.stdout) 124 | 125 | args = parser.parse_args() 126 | number_layers = int(args.number_layers) 127 | 128 | if number_layers == 2 and not math.isnan(args.fraction): 129 | parser.error('option -f cannot be used with option -n 2 ' 130 | '(fraction value is fixed)"') 131 | 132 | pgon = args.polygon_fraction 133 | points, faces, end_ang, total_ang = make_model( 134 | pgon, args.fraction, number_layers) 135 | 136 | if not points: 137 | parser.error("model is not constructible (try reducing -f value)") 138 | 139 | use_curves = args.curved_struts 140 | 141 | invisible = 9999 142 | out = anti_lib.OffFile(args.outfile) 143 | if not use_curves: 144 | out.print_header(len(points), 5 * len(faces)) 145 | out.print_verts(points) 146 | for i in range(len(faces)): 147 | out.print_face(faces[i], 0, i) 148 | for j in range(4): 149 | col = i if j else invisible 150 | out.print_face([faces[i][j], faces[i][(j+1) % 4]], 0, col) 151 | else: 152 | num_seg_pts = 60 153 | out.print_header(num_seg_pts*len(faces), (num_seg_pts-1)*len(faces)) 154 | for i in range(len(faces)): 155 | pts = [points[idx] for idx in faces[i]] 156 | axis = anti_lib.Vec.cross(pts[1]-pts[0], pts[2]-pts[0]).unit() 157 | for j in range(num_seg_pts): 158 | rot = anti_lib.Mat.rot_axis_ang( 159 | axis, total_ang * j / (num_seg_pts-1)) 160 | out.print_vert(rot * pts[1]) 161 | for i in range(len(faces)): 162 | start_idx = i * num_seg_pts 163 | for j in range(num_seg_pts-1): 164 | out.print_face([start_idx+j, start_idx+j+1], 0, i) 165 | 166 | print("end_angle=%19.17f\ntot_angle=%19.17f\nfraction=%19.17f\n" % ( 167 | end_ang, total_ang, end_ang/total_ang), file=sys.stderr) 168 | 169 | 170 | if __name__ == "__main__": 171 | main() 172 | -------------------------------------------------------------------------------- /anti_lib_progs/sph_circles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2003-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Distribute points on horizontal circles on a sphere (like a disco ball). 24 | The sphere is split into equal width bands. Balls with a diameter of this 25 | width are distributed equally around each band. The number of balls is 26 | either as many points as will fit in the band, or a specified number. 27 | The output is a list of ball centres. 28 | ''' 29 | 30 | import argparse 31 | import sys 32 | import math 33 | import anti_lib 34 | from anti_lib import Vec 35 | 36 | 37 | def calc_points(args): 38 | points = [] 39 | vert_ang_inc = math.pi/(args.number_circles-1) 40 | ball_dia = math.sqrt(2 - 2*math.cos(vert_ang_inc)) 41 | num_circles = args.number_circles 42 | horz_stagger = 0.0 43 | circles = [] 44 | for circle in range(num_circles): 45 | if args.points_list: 46 | num_balls = args.points_list[circle] 47 | if num_balls == -1: 48 | num_balls = args.default_number_points 49 | else: 50 | num_balls = args.default_number_points 51 | 52 | vert_ang = circle*vert_ang_inc 53 | if num_balls == -1: 54 | rad = math.sin(vert_ang) 55 | try: 56 | num_balls = int(math.floor(2*math.pi / 57 | math.acos((2*rad*rad-ball_dia*ball_dia) / 58 | (2*rad*rad)) * args.aspect_ratio)) 59 | except: 60 | num_balls = 1 61 | 62 | horz_ang_inc = 2*math.pi/num_balls if num_balls > 0 else 0 63 | circles.append([vert_ang, num_balls, horz_ang_inc, 0.0]) 64 | 65 | if args.stagger: 66 | for circle in range(num_circles//2, -1, -1): 67 | if circle == num_circles//2: 68 | circles[circle][3] = circles[circle][2]/4 69 | circles[circle+1][3] = -circles[circle][2]/4 70 | else: 71 | for circ in [[circle, circle+1], 72 | [num_circles-circle-1, num_circles-circle-2]]: 73 | horz_stagger = circles[circ[1]][3] 74 | if horz_stagger > 0: 75 | horz_stagger -= circles[circ[1]][2]/2 76 | else: 77 | horz_stagger += circles[circ[1]][2]/2 78 | circles[circ[0]][3] = horz_stagger 79 | 80 | for circle in range(args.exclude_poles, num_circles-args.exclude_poles): 81 | vert_ang = circles[circle][0] 82 | num_balls = circles[circle][1] 83 | horz_ang_inc = circles[circle][2] 84 | horz_stagger = circles[circle][3] 85 | rad = math.sin(vert_ang) 86 | 87 | for n in range(num_balls): 88 | horz_ang = n*horz_ang_inc+horz_stagger 89 | points.append(Vec(rad*math.cos(horz_ang), rad*math.sin(horz_ang), 90 | math.cos(vert_ang))) 91 | 92 | return points 93 | 94 | 95 | def read_nonnegative_int_list(str_val): 96 | val = [] 97 | for cnt in str_val.split(','): 98 | if cnt == '': 99 | val.append(-1) 100 | else: 101 | try: 102 | val.append(anti_lib.read_positive_int(cnt, min_val=0)) 103 | except: 104 | raise argparse.ArgumentTypeError( 105 | 'circle point counts must be non-negative integers ' 106 | 'separated only by commas') 107 | return val 108 | 109 | 110 | def main(): 111 | """Entry point""" 112 | parser = argparse.ArgumentParser(description=__doc__) 113 | 114 | num_group = parser.add_mutually_exclusive_group() 115 | num_group.add_argument( 116 | '-n', '--number-circles', 117 | help='number of circles of points (default: 6)', 118 | type=anti_lib.read_positive_int, 119 | default=6) 120 | num_group.add_argument( 121 | '-p', '--points-list', 122 | help='number of points on each circles', 123 | type=read_nonnegative_int_list) 124 | parser.add_argument( 125 | '-d', '--default-number-points', 126 | help='number of points to put on a circle if not ' 127 | 'otherwise specified (default: maximum number ' 128 | 'that fit)', 129 | type=anti_lib.read_positive_int, 130 | default=-1) 131 | parser.add_argument( 132 | '-s', '--stagger', 133 | help='stagger points between layers', 134 | action='store_true') 135 | parser.add_argument( 136 | '-x', '--exclude-poles', 137 | help='exclude the pole point circles', 138 | action='store_true') 139 | parser.add_argument( 140 | '-a', '--aspect-ratio', 141 | help='angular aspect ratio (default: 1.0)', 142 | type=anti_lib.read_positive_float, 143 | default=1.0) 144 | parser.add_argument( 145 | '-o', '--outfile', 146 | help='output file name (default: standard output)', 147 | type=argparse.FileType('w'), 148 | default=sys.stdout) 149 | 150 | args = parser.parse_args() 151 | 152 | if args.points_list: 153 | args.number_circles = len(args.points_list) 154 | 155 | points = calc_points(args) 156 | 157 | out = anti_lib.OffFile(args.outfile) 158 | out.print_all(points, []) 159 | 160 | if __name__ == "__main__": 161 | main() 162 | -------------------------------------------------------------------------------- /anti_lib_progs/sph_saff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2003-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Distribute num_points (default 20) on a sphere using the algorithm from 24 | "Distributing many points on a sphere" by E.B. Saff and 25 | A.B.J. Kuijlaars, Mathematical Intelligencer 19.1 (1997) 5--11. 26 | 27 | An implementation of an "Easy method for a fairly good point distribution 28 | [Saff/Kuijlaars]" http://www.math.niu.edu/~rusin/known-math/97/spherefaq 29 | The angle offset option uses Anton Sherwood's method for spirals 30 | based on the golden ratio http://www.ogre.nu/pack/pack.htm 31 | The output can be run through conv_hull to create a polyhedron. 32 | ''' 33 | 34 | import argparse 35 | import sys 36 | import math 37 | import anti_lib 38 | from anti_lib import Vec 39 | 40 | 41 | def calc_points(args): 42 | points = [] 43 | use_angle = 'angle' in args and args.angle is not None 44 | if use_angle: 45 | ang = (args.angle * math.pi/180) % (2*math.pi) 46 | 47 | N = args.number_points 48 | for k in range(1, N + 1): 49 | h = -1 + 2 * (k - 1) / float(N - 1) 50 | theta = math.acos(h) 51 | if k == 1 or k == N: 52 | phi = 0 53 | elif use_angle: 54 | phi += ang 55 | else: 56 | phi += 3.6 / math.sqrt(N * (1 - h * h)) 57 | 58 | points.append(Vec(math.sin(phi) * math.sin(theta), 59 | math.cos(phi) * math.sin(theta), 60 | -math.cos(theta))) 61 | phi %= 2*math.pi 62 | 63 | return points 64 | 65 | 66 | def main(): 67 | """Entry point""" 68 | parser = argparse.ArgumentParser(description=__doc__) 69 | 70 | parser.add_argument( 71 | 'number_points', 72 | help='number of points to distribute on a sphere', 73 | type=anti_lib.read_positive_int, 74 | nargs='?', 75 | default=100) 76 | parser.add_argument( 77 | '-a', '--angle', 78 | help='increment each point placement by a fixed angle instead ' 79 | 'of using the Saff and Kuiljaars placement method', 80 | type=float) 81 | parser.add_argument( 82 | '-x', '--exclude-poles', 83 | help='exclude the pole point circles', 84 | action='store_true') 85 | parser.add_argument( 86 | '-o', '--outfile', 87 | help='output file name (default: standard output)', 88 | type=argparse.FileType('w'), 89 | default=sys.stdout) 90 | 91 | args = parser.parse_args() 92 | 93 | points = calc_points(args) 94 | start = int(args.exclude_poles) 95 | end = len(points)-start 96 | 97 | out = anti_lib.OffFile(args.outfile) 98 | out.print_all(points[start:end], []) 99 | 100 | if __name__ == "__main__": 101 | main() 102 | -------------------------------------------------------------------------------- /anti_lib_progs/sph_spiral.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2003-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Distribute points in a spiral on a sphere. 24 | ''' 25 | 26 | import argparse 27 | import sys 28 | import math 29 | import anti_lib 30 | from anti_lib import Vec 31 | 32 | 33 | def angle_to_point(a, number_turns): 34 | a2 = 2 * a * number_turns # angle turned around y axis 35 | r = math.sin(a) # distance from y axis 36 | y = math.cos(a) 37 | x = r * math.sin(a2) 38 | z = r * math.cos(a2) 39 | return Vec(x, y, z) 40 | 41 | 42 | # binary search for angle with distance rad from point with angle a0 43 | def psearch(a1_delt, a1, a0, rad, number_turns): 44 | a_test = a1_delt + (a1 - a1_delt) / 2.0 45 | dist = (angle_to_point(a_test, number_turns) - 46 | angle_to_point(a0, number_turns)).mag() 47 | eps = 1e-5 48 | if rad + eps > dist > rad - eps: 49 | return a_test 50 | elif rad < dist: # Search in first interval 51 | return psearch(a1_delt, a_test, a0, rad, number_turns) 52 | else: # Search in second interval 53 | return psearch(a_test, a1, a0, rad, number_turns) 54 | 55 | 56 | def calc_points(args): 57 | points = [] 58 | number_turns = args.number_turns 59 | if not number_turns: 60 | number_turns = 1e-12 61 | if args.distance_between_points: 62 | rad = 2*args.distance_between_points 63 | else: 64 | # half distance between turns on a rad 1 sphere 65 | rad = 2*math.sqrt(1 - math.cos(math.pi/(number_turns-1))) 66 | a0 = 0 67 | cur_point = Vec(0.0, 1.0, 0.0) 68 | points.append(cur_point) 69 | 70 | delt = math.atan(rad / 2) / 10 71 | a1 = a0 + .0999999 * delt # still within sphere 72 | while a1 < math.pi: 73 | if (cur_point - angle_to_point(a1, number_turns)).mag() > rad: 74 | a0 = psearch(a1 - delt, a1, a0, rad, number_turns) 75 | cur_point = angle_to_point(a0, number_turns) 76 | points.append(cur_point) 77 | a1 = a0 78 | 79 | a1 += delt 80 | 81 | return points 82 | 83 | 84 | def main(): 85 | """Entry point""" 86 | parser = argparse.ArgumentParser(description=__doc__) 87 | 88 | parser.add_argument( 89 | 'number_turns', 90 | help='number of times the spiral turns around the ' 91 | 'axis (default: 10)', 92 | type=float, 93 | nargs='?', 94 | default=10) 95 | parser.add_argument( 96 | 'distance_between_points', 97 | help='the distance between consequetive points ' 98 | '(default: the distance beteen spiral turns)', 99 | type=anti_lib.read_positive_float, 100 | nargs='?') 101 | parser.add_argument( 102 | '-o', '--outfile', 103 | help='output file name (default: standard output)', 104 | type=argparse.FileType('w'), 105 | default=sys.stdout) 106 | 107 | args = parser.parse_args() 108 | 109 | points = calc_points(args) 110 | 111 | out = anti_lib.OffFile(args.outfile) 112 | out.print_all(points, []) 113 | 114 | if __name__ == "__main__": 115 | main() 116 | -------------------------------------------------------------------------------- /anti_lib_progs/spiro.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Spirograph generator. Output is in OFF format. 24 | ''' 25 | 26 | import argparse 27 | import sys 28 | import math 29 | import fractions 30 | 31 | 32 | def spiro(num_teeth_fixed, num_teeth_move, height, num_segs, outfile): 33 | N = abs(num_teeth_fixed) 34 | D = abs(num_teeth_move) 35 | side_sign = num_teeth_move/D 36 | height = height*D/N 37 | num_segs = num_segs 38 | 39 | turns = D/math.gcd(N, D) 40 | print('OFF\n{} 1 0'.format(num_segs), file=outfile) 41 | 42 | for i in range(num_segs): 43 | ang_fixed = 2*math.pi*turns*i/num_segs 44 | ang_move = side_sign * ang_fixed * N/D 45 | move_cent = [math.cos(ang_fixed)*(N + side_sign*D)/N, 46 | math.sin(ang_fixed)*(N + side_sign*D)/N, 0] 47 | move_offset = [height*math.cos(ang_fixed+ang_move), 48 | height*math.sin(ang_fixed+ang_move), 0] 49 | P = [move_cent[i] + move_offset[i] for i in range(3)] 50 | print(P[0], P[1], P[2], file=outfile) 51 | 52 | print(num_segs, *[i for i in range(num_segs)], file=outfile) 53 | 54 | 55 | def main(): 56 | """Entry point""" 57 | parser = argparse.ArgumentParser(description=__doc__) 58 | 59 | parser.add_argument( 60 | 'num_teeth_fixed', 61 | help='number of teeth of fixed gear wheel', 62 | type=int, 63 | nargs='?', 64 | default=20) 65 | parser.add_argument( 66 | 'num_teeth_move', 67 | help='number of teeth on moving gear wheel (positive to run on ' 68 | 'outside fixed wheel or negative to run on inside)', 69 | type=int, 70 | nargs='?', 71 | default=-8) 72 | parser.add_argument( 73 | 'height', 74 | help='height of drawing point on moving gear from its centre, ' 75 | 'as ratio of moving gear radius', 76 | type=float, 77 | nargs='?', 78 | default=1.0) 79 | parser.add_argument( 80 | '-n', '--num-segs', 81 | help='number of segments used to draw pattern', 82 | type=int, 83 | default=1000) 84 | parser.add_argument( 85 | '-o', '--outfile', 86 | help='output file name (default: standard output)', 87 | type=argparse.FileType('w'), 88 | default=sys.stdout) 89 | 90 | args = parser.parse_args() 91 | 92 | if args.num_teeth_fixed < 2: 93 | parser.error('number of teeth on fixed gear wheel cannot be ' 94 | 'less than 2') 95 | 96 | if abs(args.num_teeth_move) < 2: 97 | parser.error('number of teeth on moving gear wheel cannot be ' 98 | 'less than 2') 99 | 100 | if args.num_segs < 2: 101 | parser.error('number of segments cannot be less than 2') 102 | 103 | spiro(args.num_teeth_fixed, args.num_teeth_move, 104 | args.height, args.num_segs, args.outfile) 105 | 106 | if __name__ == "__main__": 107 | main() 108 | -------------------------------------------------------------------------------- /anti_lib_progs/str_art.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Simple epicycloid string art patterns. Output is in OFF format. 24 | ''' 25 | 26 | import argparse 27 | import sys 28 | import math 29 | import fractions 30 | 31 | 32 | def main(): 33 | """Entry point""" 34 | parser = argparse.ArgumentParser(description=__doc__) 35 | 36 | parser.add_argument( 37 | 'factor', 38 | help='an integer or fraction greater than 1', 39 | type=fractions.Fraction, 40 | nargs='?', 41 | default=2) 42 | parser.add_argument( 43 | 'num_pins', 44 | help='number of pins', 45 | type=int, 46 | nargs='?', 47 | default=120) 48 | parser.add_argument( 49 | '-o', '--outfile', 50 | help='output file name (default: standard output)', 51 | type=argparse.FileType('w'), 52 | default=sys.stdout) 53 | 54 | args = parser.parse_args() 55 | if args.factor < 1: 56 | parser.error('factor cannot be less than 1') 57 | if args.num_pins < 2: 58 | parser.error('number of pins cannot be less than 2') 59 | 60 | N = args.factor.numerator 61 | D = args.factor.denominator 62 | pins = args.num_pins 63 | turns = N / math.gcd(N, D) 64 | strings = turns*pins 65 | print('OFF\n{} {} 0'.format(pins, strings), file=args.outfile) 66 | 67 | for i in range(pins): 68 | print(math.cos(2*math.pi*i/pins), math.sin(2*math.pi*i/pins), 0, 69 | file=args.outfile) 70 | 71 | for i in range(pins): 72 | print(2, (i * N) % pins, (i * D) % pins, file=args.outfile) 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /anti_lib_progs/temcor_dome.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2003-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Make a Temcor-style dome, using the method described in 24 | https://groups.google.com/d/msg/geodesichelp/hJ3V9Nfp3kE/nikgoBPSFfwJ . 25 | The base model is a pyramid with a unit edge base polygon 26 | at a specified height above the origin. The axis to rotate the plane 27 | about passes through the origin and is in the direction of the base 28 | polygon mid-edge to the pyramid apex. 29 | ''' 30 | 31 | import argparse 32 | import sys 33 | import anti_lib 34 | from anti_lib import Vec, Mat 35 | 36 | 37 | def calc_temcor_side(pgon, pyramid_ht, base_ht, freq): 38 | freq += 1 39 | inrad = pgon.inradius() 40 | axis = Vec(-inrad, 0, pyramid_ht) # axis to rotate plane around 41 | A = Vec(0, 0, base_ht + pyramid_ht) # apex 42 | B = Vec(inrad, 0.5, base_ht) # base polygon vertex 43 | 44 | n0 = Vec.cross(A, B).unit() 45 | 46 | edge_ang = anti_lib.angle_around_axis(Vec(B[0], 0, B[2]), B, axis) 47 | ang_inc = edge_ang/freq 48 | 49 | points = [] 50 | faces = [] 51 | for i in range(freq): 52 | n_inc = Mat.rot_axis_ang(axis, i*ang_inc) * Vec(0, 1, 0) 53 | edge_v = Vec.cross(n_inc, n0).unit() 54 | last_idx = i*(i-1)//2 55 | new_idx = i*(i+1)//2 56 | for j in range(i + 1): 57 | v = Mat.rot_axis_ang(axis, -2*j*ang_inc) * edge_v 58 | points.append(v) 59 | if new_idx and j < i: 60 | faces.append([new_idx+j, new_idx+j+1, last_idx+j]) 61 | if j < i-1: 62 | faces.append([new_idx+j+1, last_idx+j+1, last_idx+j]) 63 | 64 | return (points, faces) 65 | 66 | 67 | def main(): 68 | """Entry point""" 69 | parser = argparse.ArgumentParser(description=__doc__) 70 | 71 | parser.add_argument( 72 | 'number_sides', 73 | help='number of sides (default: 6) (or may be a ' 74 | 'polygon fraction, e.g. 5/2)', 75 | type=anti_lib.read_polygon, 76 | nargs='?', 77 | default='6') 78 | parser.add_argument( 79 | 'pyramid_ht', 80 | help='pyramid_height (default: 0.5)', 81 | type=float, 82 | nargs='?', 83 | default=0.5) 84 | parser.add_argument( 85 | 'base_ht', 86 | help='base_height (default: 0.5)', 87 | type=float, 88 | nargs='?', 89 | default=0.5) 90 | parser.add_argument( 91 | 'frequency', 92 | help='frequency (default: 5)', 93 | type=anti_lib.read_positive_int, 94 | nargs='?', 95 | default=5) 96 | parser.add_argument( 97 | '-t', '--triangle-only', 98 | help='only output one triangle section of the dome', 99 | action='store_true') 100 | parser.add_argument( 101 | '-o', '--outfile', 102 | help='output file name (default: standard output)', 103 | type=argparse.FileType('w'), 104 | default=sys.stdout) 105 | 106 | args = parser.parse_args() 107 | 108 | if abs(args.pyramid_ht + args.base_ht) < 1e-13: 109 | parser.error('base height cannot be the negative of the ' 110 | 'pyramid height') 111 | 112 | (points, faces) = calc_temcor_side(args.number_sides, args.pyramid_ht, 113 | args.base_ht, args.frequency) 114 | 115 | out = anti_lib.OffFile(args.outfile) 116 | if args.triangle_only: 117 | out.print_all(points, faces) 118 | else: 119 | out.print_all_pgon(points, faces, args.number_sides, 120 | repeat_side=True) 121 | 122 | if __name__ == "__main__": 123 | main() 124 | -------------------------------------------------------------------------------- /anti_lib_progs/tri_tiling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Create a polyhedron which tiles the sphere with congruent triangles. 24 | ''' 25 | 26 | import argparse 27 | import sys 28 | import math 29 | import anti_lib 30 | from anti_lib import Vec, Mat 31 | 32 | 33 | def get_base_points(pgon, centres=0, twist=False): 34 | N = pgon.N 35 | D = pgon.D 36 | a = pgon.angle()/2 37 | points = [Vec(0, 0, 1), Vec(0, 0, -1)] # poles 38 | A = a 39 | B = math.pi/2 * (1 - D/N) 40 | cos_lat = 1/(math.tan(A) * math.tan(B)) 41 | sin_lat = math.sqrt(1 - cos_lat**2) 42 | p_north = Vec(sin_lat, 0, cos_lat) 43 | p_south = Vec(sin_lat, 0, -cos_lat) 44 | for i in range(N): 45 | points.append(p_north.rot_z(i*2*a)) 46 | for i in range(N): 47 | points.append(p_south.rot_z(i*2*a+a)) 48 | if centres: 49 | cos_cent_lat = math.cos(B)/math.sin(A) 50 | sin_cent_lat = math.sqrt(1 - cos_cent_lat**2) 51 | cent_north = Vec(sin_cent_lat, 0, cos_cent_lat) 52 | cent_south = Vec(sin_cent_lat, 0, -cos_cent_lat) 53 | for i in range(N): 54 | points.append(cent_north.rot_z(i*2*a+a) * centres) 55 | for i in range(N): 56 | points.append(cent_south.rot_z(i*2*a) * centres) 57 | 58 | rhombi = [] 59 | for i in range(N): 60 | rhombi.append([2 + i, 0, 2 + ((i+1) % N), 2 + N + i]) 61 | for i in range(N): 62 | rhombi.append([2 + N + i, 1, 2 + N + ((i-1) % N), 2 + i]) 63 | 64 | if twist: 65 | hx = [0, 2, 2 + N, 1, 2 + N+N//2, 2 + N-N//2] 66 | axis = points[hx[0]] + points[hx[2]] + points[hx[4]] 67 | rot = Mat.rot_axis_ang(axis.unit(), 2*math.pi/3) 68 | for i in list(range(hx[5]+1, 2 + N)) + list(range(hx[4]+1, 2 + 2*N)): 69 | points[i] = rot * points[i] 70 | if centres: 71 | for r in list(range(N-N//2, N+1)) + list(range(2*N-N//2, 2*N)): 72 | points[2 + 2*N + r] = rot * points[2 + 2*N + r] 73 | for r in list(range(N-N//2, N+1)) + list(range(2*N-N//2, 2*N)): 74 | for i in range(4): 75 | for idx, p_idx in enumerate(hx): 76 | if p_idx == rhombi[r][i]: 77 | rhombi[r][i] = hx[(idx+2) % 6] 78 | break 79 | 80 | return points, rhombi 81 | 82 | 83 | def make_gyroelongated_dipyramid_model(rhombi): 84 | faces = [] 85 | for r in rhombi: 86 | faces.append([r[0], r[1], r[2]]) 87 | faces.append([r[2], r[3], r[0]]) 88 | return faces 89 | 90 | 91 | def make_scalahedron_model(rhombi): 92 | faces = [] 93 | for r in rhombi: 94 | faces.append([r[1], r[2], r[3]]) 95 | faces.append([r[3], r[0], r[1]]) 96 | return faces 97 | 98 | 99 | def make_subscalahedron_model(rhombi): 100 | faces = [] 101 | for idx, r in enumerate(rhombi): 102 | apex = 2 + len(rhombi) + idx 103 | for i in range(4): 104 | faces.append([r[i], r[(i+1) % 4], apex]) 105 | return faces 106 | 107 | 108 | def main(): 109 | """Entry point""" 110 | parser = argparse.ArgumentParser(description=__doc__) 111 | 112 | parser.add_argument( 113 | 'polygon_fraction', 114 | help='number of sides of the base polygon (N), ' 115 | 'or a fraction for star polygons (N/D) (default: 5)', 116 | default='5', 117 | nargs='?', 118 | type=anti_lib.read_polygon) 119 | parser.add_argument( 120 | 'poly_type', 121 | help='polyhedron type: ' 122 | '0 - base rhombic tiling, ' 123 | '1 - gyroelongated dipyramid, ' 124 | '2 - scalenohedron, ' 125 | '3 - subscalenohedron ' 126 | '4 - inverted subscalenohedron (default: 1)', 127 | choices=['0', '1', '2', '3', '4'], 128 | default='1') 129 | parser.add_argument( 130 | '-t', '--twist', 131 | help='twist one half of the model by 120 degrees' 132 | '(N must be odd)', 133 | action='store_true') 134 | parser.add_argument( 135 | '-o', '--outfile', 136 | help='output file name (default: standard output)', 137 | type=argparse.FileType('w'), 138 | default=sys.stdout) 139 | 140 | args = parser.parse_args() 141 | pgon = args.polygon_fraction 142 | if args.twist: 143 | msg_start = 'twist: cannot twist model, polygon fraction ' 144 | if pgon.parts > 1: 145 | parser.error(msg_start + 'is compound') 146 | msg = '' 147 | if not pgon.N % 2: 148 | msg = 'numerator' 149 | elif not pgon.D % 2: 150 | msg = 'denominator' 151 | if msg: 152 | parser.error(msg_start + msg + ' is even') 153 | 154 | if args.poly_type == '3': 155 | add_centres = 1 156 | elif args.poly_type == '4': 157 | add_centres = -1 158 | else: 159 | add_centres = 0 160 | 161 | points, rhombi = get_base_points(pgon, add_centres, args.twist) 162 | if args.poly_type == '0': 163 | faces = rhombi 164 | elif args.poly_type == '1': 165 | faces = make_gyroelongated_dipyramid_model(rhombi) 166 | elif args.poly_type == '2': 167 | faces = make_scalahedron_model(rhombi) 168 | elif args.poly_type == '3' or args.poly_type == '4': 169 | faces = make_subscalahedron_model(rhombi) 170 | else: 171 | parser.error('unknown polyhedron type') 172 | 173 | out = anti_lib.OffFile(args.outfile) 174 | out.print_all_pgon(points, faces, pgon) 175 | 176 | if __name__ == "__main__": 177 | main() 178 | -------------------------------------------------------------------------------- /anti_lib_progs/twister.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Twist two polygons placed on symmetry axes and joined by a vertex. Output 24 | model in OFF format. 25 | ''' 26 | 27 | import argparse 28 | import sys 29 | import math 30 | import re 31 | import anti_lib 32 | from anti_lib import Vec, Mat 33 | 34 | epsilon = 1e-13 35 | 36 | 37 | def make_arc(v0, v1, num_segs, start_idx): 38 | axis = Vec.cross(v0, v1).unit() 39 | ang = anti_lib.angle_around_axis(v0, v1, axis) 40 | points = [v0] 41 | faces = [] 42 | mat = Mat.rot_axis_ang(axis, ang/num_segs) 43 | for i in range(num_segs): 44 | # accumulated error 45 | points.append(mat * points[-1]) 46 | faces.append([start_idx + i, start_idx + i+1]) 47 | return points, faces 48 | 49 | 50 | def make_frame(frame_elems, axis_angle, rad, num_segs): 51 | points = [] 52 | faces = [] 53 | if frame_elems: 54 | v0 = Vec(0, 0, -1) 55 | v1 = Vec(math.sin(axis_angle), 0, -math.cos(axis_angle)) 56 | if 'r' in frame_elems: 57 | ps, fs = make_arc(v0, v1, num_segs, 0) 58 | points += ps 59 | faces += fs 60 | if 'a' in frame_elems: 61 | num_pts = len(points) 62 | points += [v0, -v0, v1, -v1] 63 | faces += [[num_pts+0, num_pts+1], [num_pts+2, num_pts+3]] 64 | 65 | points = [rad * p for p in points] 66 | faces += [[i] for i in range(len(points))] 67 | 68 | return points, faces 69 | 70 | 71 | def parse_axis_pair(axes_str): 72 | pat = re.compile('(?P[^[]+)' 73 | '\[(?P[^,]+),(?P[^\]]+)](?P.*)') 74 | match = pat.match(axes_str) 75 | if not match: 76 | raise argparse.ArgumentTypeError( 77 | 'axis pair is not in form sym_type[axis1,axis2]id_no ' 78 | 'e.g I[5,3], D5[2,2]2') 79 | 80 | axis_pair = {} 81 | sym_type = match.group('sym_type') 82 | axis_pair['sym_type_str'] = sym_type # for reporting 83 | if sym_type in ['I', 'O', 'T']: 84 | axis_pair['sym_type'] = sym_type 85 | elif sym_type[0] == "D": 86 | axis_pair['sym_type'] = 'D' 87 | if len(sym_type) == 1: 88 | raise argparse.ArgumentTypeError( 89 | 'n-fold number missing for D symmetry') 90 | try: 91 | axis_pair['dih_N'] = int(sym_type[1:]) 92 | except: 93 | raise argparse.ArgumentTypeError( 94 | 'n-fold number for D symmetry is not an integer') 95 | if axis_pair['dih_N'] < 2: 96 | raise argparse.ArgumentTypeError( 97 | 'n-fold number for D symmetry cannot be less than 2') 98 | else: 99 | raise argparse.ArgumentTypeError( 100 | 'invalid symmetry type \'%s\', should be from T, O, I, or can be ' 101 | 'D followed by an integer or fraction (e.g D5) ' 102 | % (sym_type)) 103 | 104 | axis_pair['nfolds'] = [] 105 | for i, axis in enumerate(['axis1', 'axis2']): 106 | try: 107 | axis_pair['nfolds'].append(int(match.group(axis))) 108 | except ValueError: 109 | raise argparse.ArgumentTypeError( 110 | ['first', 'second'][i] + ' axis is \'' + match.group(axis) + 111 | '\', should be a positive integer ') 112 | if axis_pair['nfolds'][i] < 2: 113 | raise argparse.ArgumentTypeError( 114 | ['first', 'second'][i] + ' axis cannot be less than 2') 115 | 116 | if not match.group('id_no'): 117 | axis_pair['id_no'] = 1 118 | else: 119 | try: 120 | axis_pair['id_no'] = int(match.group('id_no')) 121 | except ValueError: 122 | raise argparse.ArgumentTypeError( 123 | 'id_no is \'' + match.group('id_no') + 124 | '\', should be a positive integer ') 125 | if axis_pair['id_no'] < 1: 126 | raise argparse.ArgumentTypeError('id_no cannot be less than 1') 127 | 128 | return axis_pair 129 | 130 | 131 | def read_axes(axes_str): 132 | axes_str = ''.join(axes_str.split()) # remove whitespace 133 | axis_pair = parse_axis_pair(axes_str) 134 | # place greater nfold axis first 135 | if axis_pair['nfolds'][0] >= axis_pair['nfolds'][1]: 136 | nfolds = tuple(axis_pair['nfolds']) 137 | axes_in_order = True 138 | else: 139 | nfolds = tuple(axis_pair['nfolds'][::-1]) 140 | axes_in_order = False 141 | 142 | phi = (math.sqrt(5)+1)/2 143 | A = {} 144 | if axis_pair['sym_type'] == 'T': 145 | # T: (3, 3), (3, 2), (2, 2) 146 | A[(3, 3)] = [ 147 | [[1, 1, 1], [-1, -1, 1]]] 148 | A[(3, 2)] = [ 149 | [[1, 1, 1], [0, 0, 1]]] 150 | A[(2, 2)] = [ 151 | [[0, 0, 1], [1, 0, 0]]] 152 | 153 | elif axis_pair['sym_type'] == 'O': 154 | # O: (4, 4), (4, 3), (4, 2)*2, (3, 3), (3, 2)*2, (2, 2)*2 155 | A[(4, 4)] = [ 156 | [[0, 0, 1], [1, 0, 0]]] # 1 157 | A[(4, 3)] = [ 158 | [[0, 0, 1], [1, 1, 1]]] # 1 159 | A[(4, 2)] = [ 160 | [[0, 0, 1], [0, 1, 1]], # 1 161 | [[0, 0, 1], [1, 1, 0]]] # 2 162 | A[(3, 3)] = [ 163 | [[1, 1, 1], [1, -1, 1]]] # 1 164 | A[(3, 2)] = [ 165 | [[1, 1, 1], [0, -1, -1]], # 1 166 | [[1, 1, 1], [1, 0, -1]]] # 2 167 | A[(2, 2)] = [ 168 | [[0, 1, 1], [1, 0, 1]], # 1 169 | [[0, 1, 1], [0, 1, -1]]] # 2 170 | 171 | elif axis_pair['sym_type'] == 'I': 172 | # I: (5, 5), (5, 3)*2, (5, 2)*3, (3, 3)*2, (3, 2)*4, (2, 2)*4 173 | A[(5, 5)] = [ 174 | [[0, 1, phi], [0, 1, -phi]]] # 1 175 | A[(5, 3)] = [ 176 | [[0, 1, phi], [1, 1, 1]], # 1 177 | [[0, 1, phi], [phi, -1/phi, 0]]] # 2 178 | A[(5, 2)] = [ 179 | [[0, 1, phi], [0, 0, -1]], # 1 180 | [[0, 1, phi], [1, 1/phi, -phi]], # 2 181 | [[0, 1, phi], [1, 0, 0]]] # 3 182 | A[(3, 3)] = [ 183 | [[1, 1, 1], [-1/phi, 0, -phi]], # 1 184 | [[1, 1, 1], [1, -1, -1]]] # 2 185 | A[(3, 2)] = [ 186 | [[1, 1, 1], [-1, -1/phi, -phi]], # 1 187 | [[1, 1, 1], [-1, 0, 0]], # 2 188 | [[1, 1, 1], [1, -1/phi, -phi]], # 3 189 | [[1, 1, 1], [1, 1/phi, -phi]]] # 4 190 | A[(2, 2)] = [ 191 | [[0, 0, 1], [1, 1/phi, phi]], # 1 192 | [[0, 0, 1], [1/phi, phi, 1]], # 2 193 | [[0, 0, 1], [phi, 1, 1/phi]], # 3 194 | [[0, 0, 1], [1, 0, 0]]] # 4 195 | 196 | else: # axis_pair['sym_type'] == 'D': 197 | N = axis_pair['dih_N'] 198 | A[(N, 2)] = [[[0, 0, 1], [1, 0, 0]]] 199 | A[(2, 2)] = [[[1, 0, 0], 200 | [math.cos(i*math.pi/N), math.sin(i*math.pi/N), 0]] 201 | for i in range(1, 1 + N//2)] 202 | 203 | if nfolds not in A: 204 | raise argparse.ArgumentTypeError( 205 | '[%d, %d] is not a valid axis pair for symmetry type %s' % ( 206 | axis_pair['nfolds'][0], axis_pair['nfolds'][1], 207 | axis_pair['sym_type_str'])) 208 | 209 | if axis_pair['id_no'] > len(A[nfolds]): 210 | raise argparse.ArgumentTypeError( 211 | 'id_no for symmetry axes %s[%d, %d] must be less than %d' % ( 212 | axis_pair['sym_type_str'], 213 | axis_pair['nfolds'][0], axis_pair['nfolds'][1], 214 | len(A[nfolds]))) 215 | 216 | axis_pair['axes'] = [ 217 | Vec().fromlist(axis).unit() for axis in 218 | A[nfolds][axis_pair['id_no']-1]] 219 | if not axes_in_order: 220 | axis_pair['axes'] = [-axis_pair['axes'][1], -axis_pair['axes'][0]] 221 | 222 | return axis_pair 223 | 224 | 225 | def calc_polygons(pgon0, pgon1, ang, ratio, angle_between_axes): 226 | # rotate initial vertex of polygon0 by ang. Common 227 | # point lies on a line through vertex in the direction 228 | # of axis0 (z-axis). Common vertex lies on a cylinder 229 | # radius r1 with axis on axis1. 230 | 231 | # rotate model so axis1 is on z-axis, to simplify the 232 | # calculation of the intersection 233 | rot = Mat.rot_axis_ang(Vec(0, 1, 0), angle_between_axes) 234 | 235 | # initial vertex turned on axis 236 | V = Vec(pgon0.circumradius(), 0, 0).rot_z(ang) 237 | # vertex in rotated model 238 | Q = rot * V 239 | # direction of line in rotated model 240 | u = rot * Vec(0, 0, 1) 241 | 242 | # equation of line is P = Q + tu for components x y z and parameter t 243 | # equation of cylinder at z=0 is x^2 + y^2 = r1^2 244 | a = u[0]**2 + u[1]**2 245 | b = 2*(Q[0]*u[0] + Q[1]*u[1]) 246 | c = Q[0]**2 + Q[1]**2 - (pgon1.circumradius()*ratio)**2 247 | 248 | disc = b**2 - 4*a*c 249 | if disc < -epsilon: 250 | raise Exception("model is not geometrically constructible") 251 | elif disc < 0: 252 | disc = 0 253 | 254 | t = (-b - math.sqrt(disc))/(2*a) 255 | 256 | P = V + Vec(0, 0, t) # the common point 257 | 258 | points = pgon0.get_points(P) 259 | faces = pgon0.get_faces() 260 | 261 | Q = rot * P 262 | rot_inv = Mat.rot_axis_ang(Vec(0, 1, 0), -angle_between_axes) 263 | points += [rot_inv*p for p in pgon1.get_points(Q)] 264 | faces += pgon1.get_faces(pgon0.N*pgon0.parts) 265 | 266 | return points, faces 267 | 268 | 269 | def read_axis_multiplier(mult_str): 270 | mult_fract = anti_lib.RawFraction(1) 271 | if len(mult_str) > 0: 272 | try: 273 | mult_fract.read(mult_str) 274 | except ValueError as e: 275 | raise argparse.ArgumentTypeError( 276 | 'invalid multiplier fraction: ' + e.args[0]) 277 | 278 | return mult_fract 279 | 280 | 281 | def frame_type(arg): 282 | if arg.strip('ra'): 283 | raise argparse.ArgumentTypeError( 284 | 'frame type contains letters other than r, a') 285 | return arg 286 | 287 | 288 | def read_turn_angle(ang_str): 289 | if ang_str[-1] == 'e': 290 | ang_type = 'e' 291 | ang_str = ang_str[:-1] 292 | elif ang_str[-1] == 'x': 293 | ang_type = 'x' 294 | ang_str = ang_str[:-1] 295 | else: 296 | ang_type = 'd' 297 | try: 298 | ang_val = float(ang_str) 299 | except ValueError as e: 300 | raise argparse.ArgumentTypeError( 301 | 'invalid numeric value: ', e.args[0]) 302 | 303 | return [ang_val, ang_type] 304 | 305 | 306 | def main(): 307 | """Entry point""" 308 | 309 | epilog = ''' 310 | notes: 311 | Depends on anti_lib.py. Use poly_kscope to repeat the model. 312 | 313 | examples: 314 | Icosahedral model 315 | twister.py I[5,3] | poly_kscope -s I| antiview 316 | 317 | Twisted icosahedral model with hexagons 318 | twister.py I[5,3] 1 2 -a 0.5e | poly_kscope -s I| antiview 319 | 320 | Dihedral model with frame 321 | twister.py D6[6,2] 1 2 -f ra | poly_kscope -s D6 | antiview 322 | ''' 323 | 324 | parser = argparse.ArgumentParser(formatter_class=anti_lib.DefFormatter, 325 | description=__doc__, epilog=epilog) 326 | 327 | parser.add_argument( 328 | 'symmetry_axes', 329 | help=']Axes given in the form: sym_type[axis1,axis2]id_no\n' 330 | ' sym_type: rotational symmetry group, can be T, O, I,\n' 331 | ' or can be D followed by an integer (e.g D5)\n' 332 | ' axis1,axis2: rotational order of each of the two axes\n' 333 | ' id_no (default: 1): integer to select between\n' 334 | ' non-equivalent pairs of axes having the same\n' 335 | ' symmetry group and rotational orders\n' 336 | 'e.g. T[2,3], I[5,2]2, D7[7,2], D11[2,2]4\n' 337 | 'Axis pairs are from the following\n' 338 | ' T: [3, 3], [3, 2], [2, 2]\n' 339 | ' O: [4, 4], [4, 3], [4, 2]x2, [3, 3], [3, 2]x2,\n' 340 | ' [2, 2]x2\n' 341 | ' I: [5, 5], [5, 3]x2, [5, 2]x3, [3, 3]x2, [3, 2]x4,\n' 342 | ' [2, 2]x4\n' 343 | ' Dn: [n 2], [2,2]x(n/2 rounded down)\n', 344 | type=read_axes, 345 | nargs='?', 346 | default='O[4,3]') 347 | parser.add_argument( 348 | 'multiplier1', 349 | help='integer or fractional multiplier for axis 1 ' 350 | '(default: 1). If the axis is N/D and the ' 351 | 'multiplier is n/d the polygon used will be N*n/d', 352 | type=read_axis_multiplier, 353 | nargs='?', 354 | default="1") 355 | parser.add_argument( 356 | 'multiplier2', 357 | help='integer or fractional multiplier for axis 2 ' 358 | '(default: 1). If the axis is N/D and the ' 359 | 'multiplier is n/d the polygon used will be N*n/d', 360 | type=read_axis_multiplier, 361 | nargs='?', 362 | default="1") 363 | parser.add_argument( 364 | '-r', '--ratio', 365 | help='ratio of edge lengths (default: 1.0)', 366 | type=float, 367 | default=1.0) 368 | parser.add_argument( 369 | '-a', '--angle', 370 | help='amount to turn polygon on axis0 in degrees ' 371 | '(default: 0), or a value followed by \'e\', ' 372 | 'where 1.0e is half the central angle of an edge, ' 373 | 'which produces an edge-connected model (negative ' 374 | 'values may have to be specified as, e.g. -a=-1.0e), ' 375 | 'or a value followed by x, which is like e but with ' 376 | 'a half turn offset', 377 | type=read_turn_angle, 378 | default='0') 379 | parser.add_argument( 380 | '-f', '--offset', 381 | help='amount to offset the first polygon to avoid ' 382 | 'coplanarity with the second polygon, for example ' 383 | '0.0001 (default: 0.0)', 384 | type=float, 385 | default=0.0) 386 | parser.add_argument( 387 | '-F', '--frame', 388 | help='include frame elements in output, any from: ' 389 | 'r - rhombic tiling edges, ' 390 | 'a - rotation axes (default: no elements)', 391 | type=frame_type, 392 | default='') 393 | parser.add_argument( 394 | '-o', '--outfile', 395 | help='output file name (default: standard output)', 396 | type=argparse.FileType('w'), 397 | default=sys.stdout) 398 | 399 | args = parser.parse_args() 400 | 401 | axis_pair = args.symmetry_axes 402 | pgons = [] 403 | for i, m in enumerate([args.multiplier1, args.multiplier2]): 404 | try: 405 | pgons.append(anti_lib.Polygon( 406 | axis_pair['nfolds'][i]*m.N, m.D)) 407 | except Exception as e: 408 | parser.error('multiplier%d: ' % (i) + e.args[0]) 409 | 410 | axes = axis_pair['axes'] 411 | 412 | if args.angle[1] == 'e': # units: half edge central angle 413 | turn_angle = args.angle[0] * pgons[0].angle()/2 414 | elif args.angle[1] == 'x': # units: half edge central angle 415 | turn_angle = math.pi + args.angle[0] * pgons[0].angle()/2 416 | else: # units: degrees 417 | turn_angle = math.radians(args.angle[0]) 418 | 419 | sin_angle_between_axes = Vec.cross(axes[0], axes[1]).mag() 420 | if abs(sin_angle_between_axes) > 1: 421 | sin_angle_between_axes = -1 if sin_angle_between_axes < 0 else 1 422 | angle_between_axes = math.asin(sin_angle_between_axes) 423 | if(Vec.dot(axes[0], axes[1]) > 0): 424 | axes[1] *= -1 425 | 426 | try: 427 | (points, faces) = calc_polygons( 428 | pgons[0], pgons[1], turn_angle, args.ratio, angle_between_axes) 429 | except Exception as e: 430 | parser.error(e.args[0]) 431 | 432 | if args.offset: 433 | for i in range(len(faces[0])): 434 | points[i][2] += args.offset 435 | 436 | frame_rad = calc_polygons(pgons[0], pgons[1], 0, args.ratio, 437 | angle_between_axes)[0][0].mag() 438 | frame_points, frame_faces = make_frame(args.frame, angle_between_axes, 439 | frame_rad, 10) 440 | 441 | rot = Mat.rot_from_to2(Vec(0, 0, 1), Vec(1, 0, 0), axes[0], axes[1]) 442 | all_points = [rot * point for point in points+frame_points] 443 | 444 | out = anti_lib.OffFile(args.outfile) 445 | out.print_header(len(all_points), len(faces)+len(frame_faces)) 446 | out.print_verts(all_points) 447 | for i in range(pgons[0].parts+pgons[1].parts): 448 | out.print_face(faces[i], 0, int(i < pgons[0].parts)) 449 | out.print_faces(frame_faces, len(points), 2) 450 | 451 | 452 | if __name__ == "__main__": 453 | main() 454 | -------------------------------------------------------------------------------- /anti_lib_progs/twister_rhomb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Twist polygons, of the same type, placed on certain fixed axes and 24 | joined by vertices. 25 | 26 | ''' 27 | 28 | import argparse 29 | import sys 30 | import os 31 | import math 32 | import anti_lib 33 | from anti_lib import Vec, Mat 34 | 35 | epsilon = 1e-13 36 | 37 | 38 | def make_arc(v0, v1, num_segs, start_idx): 39 | axis = Vec.cross(v0, v1).unit() 40 | ang = anti_lib.angle_around_axis(v0, v1, axis) 41 | points = [v0] 42 | faces = [] 43 | mat = Mat.rot_axis_ang(axis, ang/num_segs) 44 | for i in range(num_segs): 45 | # accumulated error 46 | points.append(mat * points[-1]) 47 | faces.append([start_idx + i, start_idx + i+1]) 48 | return points, faces 49 | 50 | 51 | def make_frame(frame_elems, pgon, axis_angle, num_segs): 52 | points = [] 53 | faces = [] 54 | if frame_elems: 55 | v0 = Vec(0, 0, 1) 56 | v1 = Vec(-math.sin(axis_angle), 0, math.cos(axis_angle)) 57 | v2 = v1.rot_z(pgon.angle()/2) 58 | v2[2] *= -1 59 | if 'r' in frame_elems: 60 | ps, fs = make_arc(v0, v1, num_segs, 0) 61 | points += ps 62 | faces += fs 63 | ps, fs = make_arc(v1, v2, num_segs, num_segs+1) 64 | points += ps 65 | faces += fs 66 | if 'a' in frame_elems: 67 | faces += [[len(points)+i, len(points)+i+1] for i in range(0, 6, 2)] 68 | points += [v0, -v0, v1, -v1, v2, -v2] 69 | 70 | rad = calc_polygons(pgon, 0, axis_angle, -1)[0][0].mag() 71 | points = [rad * p for p in points] 72 | faces += [[i] for i in range(len(points))] 73 | 74 | return points, faces 75 | 76 | 77 | def calc_polygons(pgon, ang, angle_between_axes, sign_flag=1): 78 | # rotate initial vertex of first polygon by ang. Common 79 | # point lies on a line through vertex in the direction 80 | # of first axis (z-axis). Common point lies on a cylinder 81 | # with the circumradius for radius and second axis for axis. 82 | 83 | # rotate model so axis1 is on z-axis, to simplify the 84 | # calculation of the intersection 85 | rot = Mat.rot_axis_ang(Vec(0, 1, 0), angle_between_axes) 86 | 87 | R = pgon.circumradius() 88 | V = Vec(R, 0, 0).rot_z(ang) # initial vertex turned on axis 89 | Q = rot * V # vertex in rotated model 90 | u = rot * Vec(0, 0, 1) # direction of line in rotated model 91 | 92 | # equation of line is P = Q + tu for components x y z and parameter t 93 | # equation of cylinder at z=0 is x^2 + y^2 = r1^2 94 | a = u[0]**2 + u[1]**2 95 | b = 2*(Q[0]*u[0] + Q[1]*u[1]) 96 | c = Q[0]**2 + Q[1]**2 - R**2 97 | 98 | disc = b**2 - 4*a*c 99 | if disc < -epsilon: 100 | raise Exception("model is not geometrically constructible") 101 | elif disc < 0: 102 | disc = 0 103 | 104 | # The sign flag, which changes for the range 90 to 270 degrees, allows 105 | # the model to reverse, otherwise the model breaks apart in this range. 106 | t = (-b + sign_flag*math.sqrt(disc))/(2*a) 107 | 108 | P = V + Vec(0, 0, t) # the common point 109 | 110 | points = pgon.get_points(P) 111 | faces = pgon.get_faces() 112 | 113 | Q = rot * P 114 | rot_inv = Mat.rot_axis_ang(Vec(0, 1, 0), -angle_between_axes) 115 | points += [rot_inv*p for p in pgon.get_points(Q)] 116 | faces += pgon.get_faces(pgon.N*pgon.parts) 117 | 118 | return points, faces 119 | 120 | 121 | def rot_reflect_pair(points, pgon, d, rev=False): 122 | mat0 = Mat.rot_axis_ang(Vec(0, 0, 1), (d+0.5)*pgon.angle()) 123 | pts = [mat0 * Vec(p[0], p[1], -p[2]) for p in points] 124 | mat1 = Mat.rot_axis_ang(Vec(0, 0, 1), d*pgon.angle()) 125 | if rev: 126 | return [mat1 * p for p in points]+pts 127 | else: 128 | return pts + [mat1 * p for p in points] 129 | 130 | 131 | def frame_type(arg): 132 | if arg.strip('ra'): 133 | raise argparse.ArgumentTypeError( 134 | 'frame type contains letters other than r, a') 135 | return arg 136 | 137 | 138 | def read_turn_angle(ang_str): 139 | if ang_str[-1] == 'e': 140 | ang_type = 'e' 141 | ang_str = ang_str[:-1] 142 | elif ang_str[-1] == 'x': 143 | ang_type = 'x' 144 | ang_str = ang_str[:-1] 145 | else: 146 | ang_type = 'd' 147 | try: 148 | ang_val = float(ang_str) 149 | except ValueError as e: 150 | raise argparse.ArgumentTypeError( 151 | 'invalid numeric value: ', e.args[0]) 152 | 153 | return [ang_val, ang_type] 154 | 155 | 156 | def main(): 157 | """Entry point""" 158 | parser = argparse.ArgumentParser(description=__doc__) 159 | 160 | parser.add_argument( 161 | 'polygon', 162 | help='number of sides of polygon (default: 7) ' 163 | '(or may be a polygon fraction, e.g. 5/2)', 164 | type=anti_lib.read_polygon, 165 | nargs='?', 166 | default='7') 167 | parser.add_argument( 168 | 'turn_angle', 169 | help='amount to turn polygon on axis0 in degrees ' 170 | '(default (0.0), or a value followed by \'e\', ' 171 | 'where 1.0e is half the central angle of an edge, ' 172 | 'which produces an edge-connected model (negative ' 173 | 'values may have to be specified as, e.g. -a=-1.0e),' 174 | 'or a value followed by x, which is like e but with ' 175 | 'a half turn offset', 176 | type=read_turn_angle, 177 | nargs='?', 178 | default='0') 179 | parser.add_argument( 180 | '-n', '--number-faces', 181 | help='number of faces in output (default: all): ' 182 | '0 - none (frame only), ' 183 | '2 - cap and adjoining polygon' 184 | '4 - two caps and two adjoining connected polygons', 185 | type=int, 186 | choices=[0, 2, 4], 187 | default=-1) 188 | parser.add_argument( 189 | '-F', '--frame', 190 | help='include frame elements in output, any from: ' 191 | 'r - rhombic tiling edges, ' 192 | 'a - rotation axes (default: no elements)', 193 | type=frame_type, 194 | default='') 195 | parser.add_argument( 196 | '-o', '--outfile', 197 | help='output file name (default: standard output)', 198 | type=argparse.FileType('w'), 199 | default=sys.stdout) 200 | 201 | args = parser.parse_args() 202 | 203 | pgon = args.polygon 204 | N = pgon.N 205 | D = pgon.D 206 | parts = pgon.parts 207 | # if N % 2 == 0: 208 | # parser.error('polygon: %sfraction numerator must be odd' % 209 | # ('reduced ' if parts > 1 else '')) 210 | if D % 2 == 0: 211 | print(os.path.basename(__file__)+': warning: ' 212 | 'polygon: %sfraction denominator should be odd, ' 213 | 'model will only connect correctly at certain twist angles' % 214 | ('reduced ' if parts > 1 else ''), 215 | file=sys.stderr) 216 | 217 | if abs(N/D) < 3/2: 218 | parser.error('polygon: the polygon fraction cannot be less than 3/2 ' 219 | '(base rhombic tiling is not constructible)') 220 | 221 | axis_angle = math.acos(1/math.tan( 222 | math.pi*D/N)/math.tan(math.pi*(N-D)/(2*N))) 223 | if args.turn_angle[1] == 'e': # units: half edge central angle 224 | turn_angle = args.turn_angle[0] * pgon.angle()/2 225 | elif args.turn_angle[1] == 'x': # units: half edge central angle 226 | turn_angle = math.pi + args.turn_angle[0] * pgon.angle()/2 227 | else: # units: degrees 228 | turn_angle = math.radians(args.turn_angle[0]) 229 | 230 | turn_angle_test_val = abs(math.fmod(abs(turn_angle), 2*math.pi) - math.pi) 231 | sign_flag = 1 - 2*(turn_angle_test_val > math.pi/2) 232 | try: 233 | (points, faces) = calc_polygons(pgon, turn_angle, axis_angle, 234 | sign_flag) 235 | except Exception as e: 236 | parser.error(e.args[0]) 237 | 238 | if args.number_faces < 0: 239 | num_twist_faces = (2*N + 2)*parts 240 | else: 241 | num_twist_faces = args.number_faces*parts 242 | num_twist_points = num_twist_faces * N 243 | 244 | frame_points, frame_faces = make_frame(args.frame, pgon, axis_angle, 10) 245 | 246 | if num_twist_points + len(frame_points) == 0: 247 | parser.error('no output specified, use -f with -n 0 to output a frame') 248 | 249 | if D % 2 == 0: 250 | mat = Mat.rot_axis_ang(Vec(0, 0, 1), math.pi/N) 251 | points = [mat * p for p in points] 252 | 253 | out = anti_lib.OffFile(args.outfile) 254 | out.print_header(num_twist_points+2*N*len(frame_points), 255 | num_twist_faces+2*N*len(frame_faces)) 256 | if args.number_faces == -1: 257 | out.print_verts(rot_reflect_pair(points[:N*parts], pgon, 0, True)) 258 | for i in range(N): 259 | out.print_verts(rot_reflect_pair(points[N*parts:], pgon, i)) 260 | elif args.number_faces == 2: 261 | out.print_verts(points) 262 | elif args.number_faces == 4: 263 | out.print_verts(rot_reflect_pair(points[:N*parts], pgon, 0, True),) 264 | out.print_verts(rot_reflect_pair(points[N*parts:], pgon, 0)) 265 | 266 | for i in range(N): 267 | out.print_verts(rot_reflect_pair(frame_points, pgon, i)) 268 | 269 | for i in range(num_twist_faces): 270 | out.print_face(faces[0], i*N, (i//parts) % 2) 271 | 272 | for i in range(N): 273 | cur_num_points = num_twist_points + 2*i*len(frame_points) 274 | for j in [0, len(frame_points)]: 275 | out.print_faces(frame_faces, cur_num_points+j, col=2) 276 | 277 | if __name__ == "__main__": 278 | main() 279 | -------------------------------------------------------------------------------- /anti_lib_progs/twister_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2016 Adrian Rossiter 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | ''' 23 | Twist two polygons placed on axes at a specified angle and joined by a vertex 24 | ''' 25 | 26 | 27 | import argparse 28 | import sys 29 | import math 30 | import anti_lib 31 | from anti_lib import Vec, Mat 32 | 33 | epsilon = 1e-13 34 | 35 | 36 | def calc_polygons2(pgon0, r0, ang, pgon1, r1, angle_between_axes): 37 | # rotate initial vertex of polygon0 by ang. Common 38 | # point lies on a line through vertex in the direction 39 | # of axis0 (z-axis). Common vertex lies on a cylinder 40 | # radius r1 with axis on axis1. 41 | 42 | # rotate model so axis1 is on z-axis, to simplify the 43 | # calculation of the intersection 44 | rot = Mat.rot_axis_ang(Vec(0, 1, 0), angle_between_axes) 45 | 46 | # initial vertex turned on axis 47 | V = Vec(r0, 0, 0).rot_z(ang) 48 | # vertex in rotated model 49 | Q = rot * V 50 | # direction of line in rotated model 51 | u = rot * Vec(0, 0, 1) 52 | 53 | # equation of line is P = Q + tu for components x y z and parameter t 54 | # equation of cylinder at z=0 is x^2 + y^2 = r1^2 55 | a = u[0]**2 + u[1]**2 56 | b = 2*(Q[0]*u[0] + Q[1]*u[1]) 57 | c = Q[0]**2 + Q[1]**2 - r1**2 58 | 59 | disc = b**2 - 4*a*c 60 | if disc < -epsilon: 61 | raise Exception("model is not geometrically constructible") 62 | elif disc < 0: 63 | disc = 0 64 | 65 | t = (-b - math.sqrt(disc))/(2*a) # negative gives most distant point 66 | 67 | P = V + Vec(0, 0, t) # the common point 68 | 69 | points = [] 70 | points += [P.rot_z(i*pgon0.angle()) for i in range(pgon0.N)] 71 | faces = [[i for i in range(pgon0.N)]] 72 | 73 | Q = rot * P 74 | rot_inv = Mat.rot_axis_ang(Vec(0, 1, 0), -angle_between_axes) 75 | points += [rot_inv * Q.rot_z(i*pgon1.angle()) for i in range(pgon1.N)] 76 | faces += [[i+pgon0.N for i in range(pgon1.N)]] 77 | 78 | return points, faces 79 | 80 | 81 | def read_turn_angle(ang_str): 82 | if ang_str[-1] == 'e': 83 | ang_type = 'e' 84 | ang_str = ang_str[:-1] 85 | elif ang_str[-1] == 'x': 86 | ang_type = 'x' 87 | ang_str = ang_str[:-1] 88 | else: 89 | ang_type = 'd' 90 | try: 91 | ang_val = float(ang_str) 92 | except ValueError as e: 93 | raise argparse.ArgumentTypeError( 94 | 'invalid numeric value: ', e.args[0]) 95 | 96 | return [ang_val, ang_type] 97 | 98 | 99 | def main(): 100 | """Entry point""" 101 | parser = argparse.ArgumentParser(description=__doc__) 102 | 103 | parser.add_argument( 104 | 'number_sides0', 105 | help='number of sides of polygon 0 (default: 6) ' 106 | '(or may be a polygon fraction, e.g. 5/2)', 107 | type=anti_lib.read_polygon, 108 | nargs='?', 109 | default='6') 110 | parser.add_argument( 111 | 'number_sides1', 112 | help='number of sides of polygon 0 (default: 5) ' 113 | '(or may be a polygon fraction, e.g. 5/2)', 114 | type=anti_lib.read_polygon, 115 | nargs='?', 116 | default='5') 117 | parser.add_argument( 118 | '-A', '--angle_between_axes', 119 | help='angle between the two axes (default: 60 degs)', 120 | type=float, 121 | default=60.0) 122 | parser.add_argument( 123 | '-r', '--ratio', 124 | help='ratio of edge lengths (default: 1.0)', 125 | type=float, 126 | default=1.0) 127 | parser.add_argument( 128 | '-a', '--angle', 129 | help='amount to turn polygon on axis0 in degrees ' 130 | '(default: 0), or a value followed by \'e\', ' 131 | 'where 1.0e is half the central angle of an edge, ' 132 | 'which produces an edge-connected model (negative ' 133 | 'values may have to be specified as, e.g. -a=-1.0e), ' 134 | 'or a value followed by x, which is like e but with ' 135 | 'a half turn offset', 136 | type=read_turn_angle, 137 | default='0') 138 | parser.add_argument( 139 | '-x', '--x-axis-vert', 140 | help='offset of vertex of side polygon to align with x-axis, with 0' 141 | 'being the vertex attached to the axial polygon', 142 | type=int) 143 | parser.add_argument( 144 | '-o', '--outfile', 145 | help='output file name (default: standard output)', 146 | type=argparse.FileType('w'), 147 | default=sys.stdout) 148 | 149 | args = parser.parse_args() 150 | 151 | pgon0 = args.number_sides0 152 | pgon1 = args.number_sides1 153 | 154 | if args.angle[1] == 'e': # units: half edge central angle 155 | turn_angle = args.angle[0] * pgon0.angle()/2 156 | elif args.angle[1] == 'x': # units: half edge central angle 157 | turn_angle = math.pi + args.angle[0] * pgon0.angle()/2 158 | else: # units: degrees 159 | turn_angle = math.radians(args.angle[0]) 160 | 161 | try: 162 | (points, faces) = calc_polygons2( 163 | pgon0, pgon0.circumradius(), turn_angle, 164 | pgon1, args.ratio*pgon1.circumradius(), 165 | math.radians(args.angle_between_axes)) 166 | except Exception as e: 167 | parser.error(e.args[0]) 168 | 169 | if(args.x_axis_vert is not None): 170 | v = pgon0.N + args.x_axis_vert % pgon1.N 171 | P = points[v].copy() 172 | transl = Mat.transl(Vec(0, 0, -P[2])) 173 | # for i in range(len(points)): 174 | # points[i][2] -= P[2] 175 | rot = Mat.rot_xyz(0, 0, anti_lib.angle_around_axis( 176 | P, Vec(1, 0, 0), Vec(0, 0, 1))) 177 | points = [rot*transl*point for point in points] 178 | 179 | out = anti_lib.OffFile(args.outfile) 180 | out.print_all(points, faces) 181 | 182 | if __name__ == "__main__": 183 | main() 184 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | """ 3 | 4 | # Always prefer setuptools over distutils 5 | from setuptools import setup, find_packages 6 | # To use a consistent encoding 7 | from codecs import open 8 | from os import listdir 9 | from os import path 10 | 11 | here = path.abspath(path.dirname(__file__)) 12 | 13 | # Get the long description from the README file 14 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 15 | long_description = f.read() 16 | 17 | prog_dir = 'anti_lib_progs' 18 | src_path = path.join(here, prog_dir) 19 | script_progs = [] 20 | for f in listdir(src_path): 21 | if path.isfile(path.join(src_path, f)) and f not in [ 22 | '__init__.py', 'anti_lib.py']: 23 | script_progs.append(f+'='+prog_dir+'.'+f[:-3]+':main') 24 | 25 | setup( 26 | name='antiprism_python', 27 | version='0.1.3', 28 | description='Scripts to generate various types of polyhedra', 29 | long_description=long_description, 30 | url='https://github.com/antiprism/antiprism_python', 31 | 32 | author='Adrian Rossiter', 33 | author_email='adrian@antiprism.com', 34 | 35 | license='MIT', 36 | 37 | classifiers=[ 38 | 'Development Status :: 4 - Beta', 39 | 'Environment :: Console', 40 | 'Intended Audience :: Science/Research', 41 | 'Topic :: Scientific/Engineering :: Mathematics', 42 | 'Topic :: Scientific/Engineering :: Visualization', 43 | 'License :: OSI Approved :: MIT License', 44 | 'Programming Language :: Python :: 3 :: Only', 45 | ], 46 | 47 | keywords='antiprism polyhdron polyhedra', 48 | 49 | # You can just specify the packages manually here if your project is 50 | # simple. Or you can use find_packages(). 51 | packages=find_packages(exclude=[]), 52 | py_modules=['anti_lib'], 53 | 54 | entry_points={ 55 | 'console_scripts': script_progs, 56 | }, 57 | ) 58 | --------------------------------------------------------------------------------