├── conway_example.blend ├── images ├── conway_aD.png ├── conway_cgC.png ├── conway_CcCgC.png ├── conway_CcgC.png ├── conway_aagD.png ├── conway_examples.png ├── code_notes-030bc.png ├── code_notes-2fb53.png └── conway_kg_hexa_grid.png ├── snl_kis.py ├── snl_chamfer.py ├── snl_canon.py ├── snl_plato.py ├── snl_conway_op.py ├── LICENSE ├── canon.py ├── code_notes.md ├── README.md ├── test_conway.py └── conway.py /conway_example.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/conway_polyhedron_operators/HEAD/conway_example.blend -------------------------------------------------------------------------------- /images/conway_aD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/conway_polyhedron_operators/HEAD/images/conway_aD.png -------------------------------------------------------------------------------- /images/conway_cgC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/conway_polyhedron_operators/HEAD/images/conway_cgC.png -------------------------------------------------------------------------------- /images/conway_CcCgC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/conway_polyhedron_operators/HEAD/images/conway_CcCgC.png -------------------------------------------------------------------------------- /images/conway_CcgC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/conway_polyhedron_operators/HEAD/images/conway_CcgC.png -------------------------------------------------------------------------------- /images/conway_aagD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/conway_polyhedron_operators/HEAD/images/conway_aagD.png -------------------------------------------------------------------------------- /images/conway_examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/conway_polyhedron_operators/HEAD/images/conway_examples.png -------------------------------------------------------------------------------- /images/code_notes-030bc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/conway_polyhedron_operators/HEAD/images/code_notes-030bc.png -------------------------------------------------------------------------------- /images/code_notes-2fb53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/conway_polyhedron_operators/HEAD/images/code_notes-2fb53.png -------------------------------------------------------------------------------- /images/conway_kg_hexa_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elfnor/conway_polyhedron_operators/HEAD/images/conway_kg_hexa_grid.png -------------------------------------------------------------------------------- /snl_kis.py: -------------------------------------------------------------------------------- 1 | """ 2 | in height s d=0.1 n=1 3 | in verts_in v d=[] n=1 4 | in faces_in s d=[] n=1 5 | out verts_out v 6 | out faces_out s 7 | """ 8 | 9 | from conway import kis 10 | 11 | verts_kis, faces_kis = kis(verts_in, faces_in, height) 12 | 13 | faces_out.append(faces_kis) 14 | verts_out.append(verts_kis) 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /snl_chamfer.py: -------------------------------------------------------------------------------- 1 | """ 2 | in thickness s d=0.34 n=1 3 | in height s d=0.22 n=1 4 | in verts_in v d=[] n=1 5 | in faces_in s d=[] n=1 6 | out verts_out v 7 | out faces_out s 8 | """ 9 | 10 | from conway import chamfer 11 | 12 | verts_chamfer, faces_chamfer = chamfer(verts_in, faces_in, thickness, height) 13 | 14 | faces_out.append(faces_chamfer) 15 | verts_out.append(verts_chamfer) 16 | -------------------------------------------------------------------------------- /snl_canon.py: -------------------------------------------------------------------------------- 1 | """ 2 | in iterations s d=20 n=1 3 | in scale_factor s d=0.1 n=1 4 | in verts_in v d=[] n=1 5 | in faces_in s d=[] n=1 6 | out verts_out v 7 | """ 8 | 9 | import mathutils 10 | import bpy 11 | canon = bpy.data.texts["canon.py"].as_module() 12 | 13 | 14 | 15 | 16 | verts_new = [mathutils.Vector(v_xyz) for v_xyz in verts_in] 17 | 18 | verts_canon = canon.canonize(verts_new, faces_in, iterations, scale_factor) 19 | 20 | verts_canon = [list(verts_canon[v_i]) for v_i, v_xyz in enumerate(verts_new)] 21 | verts_out.append(verts_canon) 22 | 23 | 24 | -------------------------------------------------------------------------------- /snl_plato.py: -------------------------------------------------------------------------------- 1 | """ 2 | in dummy s 3 | enum = tetra octa cube dodeca icosa 4 | out vertices v 5 | out faces s 6 | """ 7 | 8 | plato = {'tetra': "4",'cube': '6', 'octa': '8', 'dodeca': '12', 'icosa': '20'} 9 | 10 | from add_mesh_extra_objects.add_mesh_solid import source 11 | 12 | def ui(self, context, layout): 13 | layout.prop(self, 'custom_enum', expand=False) 14 | 15 | 16 | vectors, faces = source(plato[self.custom_enum]) 17 | vertices = [list(v) for v in vectors] 18 | 19 | # match nesting of other Sverchok generators 20 | vertices = [vertices] 21 | faces = [faces] -------------------------------------------------------------------------------- /snl_conway_op.py: -------------------------------------------------------------------------------- 1 | """ 2 | in verts_in v d=[] n=1 3 | in faces_in s d=[] n=1 4 | enum = identity kis dual ambo chamfer gyro propellor whirl 5 | out verts_out v 6 | out faces_out s 7 | """ 8 | 9 | import bpy 10 | conway = bpy.data.texts["conway.py"].as_module() 11 | 12 | def ui(self, context, layout): 13 | layout.prop(self, 'custom_enum', expand=False) 14 | 15 | 16 | if self.custom_enum == 'identity': 17 | faces_op = faces_in 18 | verts_op = verts_in 19 | else: 20 | cw_op = getattr(conway, self.custom_enum) 21 | verts_op, faces_op = cw_op(verts_in, faces_in) 22 | 23 | 24 | faces_out.append(faces_op) 25 | verts_out.append(verts_op) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /canon.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | """ 4 | functions used in 'canonicalization' of polyhedra 5 | 6 | This canonical form of a polyhedron is where the verticies of the polyhedron are positioned such that: 7 | all the edges are tangent to the unit sphere, 8 | the origin is the center of gravity of the points at which the edges touch the sphere, 9 | the faces are flat (i.e. the vertices of each face lie in some plane), but are not necessarily regular. 10 | """ 11 | 12 | import copy 13 | import mathutils 14 | import bpy 15 | cw = bpy.data.texts["conway.py"].as_module() 16 | 17 | 18 | def tangentify(verts, faces, scale): 19 | """ 20 | For each edge, find the closest point to the origin 21 | move the two end points of the edge so the edge is closer to tangent to 22 | the unit sphere 23 | """ 24 | verts_tang = copy.deepcopy(verts) 25 | for face in faces: 26 | for v1, v2 in zip(face, face[1:] + face[:1]): 27 | if v1 < v2: 28 | va_xyz, s = mathutils.geometry.intersect_point_line( 29 | mathutils.Vector(), 30 | verts[v1], verts[v2]) 31 | c = scale * 0.5 * (1 - va_xyz.length) * va_xyz 32 | 33 | verts_tang[v1] = verts_tang[v1] + c 34 | verts_tang[v2] = verts_tang[v2] + c 35 | 36 | return verts_tang 37 | 38 | 39 | def recenter(verts, faces): 40 | """ 41 | move verts so center of tangent points is at origin 42 | """ 43 | nedges = 0 44 | vsum = mathutils.Vector() 45 | for face in faces: 46 | for v1, v2 in zip(face, face[1:] + face[:1]): 47 | if v1 < v2: 48 | nedges += 1 49 | va_xyz, s = mathutils.geometry.intersect_point_line( 50 | mathutils.Vector(), 51 | verts[v1], verts[v2]) 52 | vsum = vsum + va_xyz 53 | 54 | center_xyz = vsum/nedges 55 | verts_cent = [v_xyz - center_xyz for v_xyz in verts] 56 | return verts_cent 57 | 58 | 59 | def planarize(verts, faces, scale): 60 | """ 61 | move verts in each face closer to a plane defined by the face normal 62 | direction and the face centroid 63 | """ 64 | verts_plane = copy.deepcopy(verts) 65 | for face in faces: 66 | center_xyz = mathutils.Vector(cw.face_center(verts, face)) 67 | norm = cw.face_normal(verts, face) 68 | for v1 in face: 69 | r1 = center_xyz - verts[v1] 70 | r2 = scale * norm 71 | r4 = r2.dot(r1) * norm 72 | verts_plane[v1] = verts_plane[v1] + r4 73 | return verts_plane 74 | 75 | def canonize(verts_new, faces_in, iterations, scale_factor): 76 | """ 77 | repeat tangentify, recenter, planarize for iterations 78 | """ 79 | for i in range(iterations): 80 | verts_old = copy.deepcopy(verts_new) 81 | verts_new = tangentify(verts_new, faces_in, scale_factor) 82 | verts_new = recenter(verts_new, faces_in) 83 | verts_new = planarize(verts_new, faces_in, scale_factor) 84 | max_change = max([abs((verts_old[v1] - verts_new[v1]).length) 85 | for v1, v_xyz in enumerate(verts_old)]) 86 | if max_change < 1e-8: 87 | break 88 | return verts_new -------------------------------------------------------------------------------- /code_notes.md: -------------------------------------------------------------------------------- 1 | # Comments on the coding of *conway.py* 2 | 3 | ### Introduction 4 | 5 | Conway Polyhedra are formed by applying various operators to a seed polyhedron such as one of the platonic solids. 6 | 7 | These are some high level comments on the code in *conway.py* which implements the operators in python. 8 | 9 | ### Mesh Representation 10 | 11 | The basic structure used to encode a mesh is the face-vertex representation. A vertex is represented by a list of three coordinates (x, y, z). A mesh will have a list of vertices in no particular order. The faces are a list of the index of the vertices in the face in counter-clockwise order. A mesh will haves a list of faces, again in no particular order. Each Conway operator has a list of vertices and a list of faces as input and output. 12 | 13 | Internally the code uses what I've called a flag-tags representation. This is partly borrowed from [Levskaya's coffeescript code](https://github.com/levskaya/polyhedronisme/blob/master/topo_operators.coffee) During the application of an operator to a mesh the vertices and faces of the mesh are created step by step. Each newly created face and vertex is given a string "tag" that uniquely identifies it. This is more versatile than referring directly to the index of a vertex in the vertices list as it allows you to refer to new vertices by their position in the mesh structure before they are created and before you know their index number. 14 | 15 | The mesh is represented by a dictionary of "flags" or directed half edges of the mesh. A flag consists of a vertex, a face it belongs to and the next CCW vertex in that face. It is coded as a dictionary where the key is the face tag concatenated with the vertex tag. The value is a tuple (face tag, vert tag, tag of next CCW vert in the face). There will be two flags for every edge in the mesh. 16 | 17 | The following image shows two flags and their associated tags for a cube. The whole cube will have 24 flags. 18 | 19 | ![cube_flags](images/code_notes-2fb53.png) 20 | 21 | When new edges and vertices are created by an operator they are given tags based on the conventions below. Vertex tags always begin with "v" and face tags with "f". The following image shows the new tags created by an ambo operator on a cube. The vertex tags have a dark blue background and the face tags are pink. 22 | 23 | ![ambo_tags](images/code_notes-030bc.png) 24 | 25 | At the end of the creation process the flag-tag structure is converted back to a face-vertex representation. 26 | 27 | ### Tag naming conventions 28 | 29 | * vert at original vert with index 3 is tagged 'v3' 30 | * vert at centre of face with index 5 is tagged 'vf5' 31 | * vert one third along edge between v3 and v4 is tagged 'v3:4' 32 | * vert two thirds along edge between v3 and v4 is tagged 'v4:3' 33 | * vert halfway along edge between v3 and v4 is tagged 'v3:4' never 'v4:3' 34 | * face on original face 5 contained original vert 3 is tagged 'f5:3' 35 | * face at centre of orginal face 5 is tagged 'f5' 36 | * face centered on original vert 3 is tagged 'fv3' 37 | 38 | ### Iterating over edges 39 | 40 | The *zip* function is used to iterate over every edge in a face. That is take each consecutive pair of vertices including the last vertex and the first vertex. 41 | 42 | ``` 43 | for face in faces: 44 | for v1, v2 in zip(face, face[1:] + face[:1]): 45 | ... 46 | ``` 47 | Iterating over each edge like this will cover each edge of the mesh twice, once in the order (v5, v6) and once in the order (v6, v5). 48 | 49 | ### Variable naming conventions 50 | 51 | In the code it can be confusing whether the variable refers to the coordinates of a vertex or its index or its tag. A variable naming convention has been used to give light type casting. 52 | 53 | for a vertex use the following variable names 54 | 55 | * v3_xyz - coordinates (list of three floats) 56 | * v3_i - index (integer) 57 | * v3 - also index 58 | * v3_t - tag (string) 59 | * v3_v - mathutils.Vector(v3_xyz) 60 | 61 | for a face 62 | 63 | * faces - list of a list of vertex indexes (list of lists of integers) 64 | * face - list of vertex indexes (list of integers) 65 | * face_i - that face's index in faces (integer) 66 | * face_t - tag to name face (string) 67 | * face_vt - list of vertex tags (list of strings) 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Conway Operators in python 2 | 3 | Updated to Blender 2.83 4 | 5 | [Conway Polyhedra](https://en.wikipedia.org/wiki/Conway_polyhedron_notation) are formed by applying various operators to a seed polyhedron such as one of the platonic solids. 6 | 7 | from [Wikipedia](https://en.wikipedia.org/wiki/Conway_polyhedron_notation) 8 | 9 | ![conway operators](/images/conway_examples.png) 10 | 11 | This repo includes python code *conway.py* to implement a subset of these operators. 12 | 13 | The code is designed to be used with [Blender](https://www.blender.org/) and [Sverchok](https://github.com/nortikin/sverchok/) scripted nodes but the only dependency is Blender's mathutils library. This means the code can be run outside Blender using a [standalone version of the mathutils module](https://github.com/majimboo/py-mathutils). 14 | 15 | ## Usage Notes 16 | 17 | Using the *conway.py* module in Blender with Sverchok's *Scripted Node Lite*. 18 | * Install [Sverchok](https://github.com/nortikin/sverchok/) in [Blender](https://www.blender.org/). 19 | * Download the [zip file]() from github 20 | * Open *conway.py* as a text block in Blender. 21 | * Open *snl_plato.py* and *snl_conway_op.py* as text blocks in Blender. These contain code for each Sverchok *Scripted Node Lite*. 22 | * In a *Node Editor* view create a new *Node Tree* and add two *Scripted Node Lite* nodes. 23 | * Use the notebook icon on the node to select *snl_plato.py* on the left node and *snl_comway_op.py* on the right node. Click the plug icon on each node to load the code. 24 | * Wire up the nodes along with a *Viewer Draw* node as shown below. 25 | 26 | ![conway nodes](/images/conway_aD.png) 27 | 28 | Wire up multiple copies of *snl_conway_op.py* in a row to produce more complex shapes. 29 | 30 | ![conway_aagD](/images/conway_aagD.png) 31 | 32 | Two of the operators *kis* and *chamfer* can take parameters such as the height of the *kis* pyramid or the *height* and *thickness* of the *chamfer*. There is a separate *Scripted Node Lite* given for these two operators with sliders for the parameters. 33 | 34 | Some operators, particularly *gyro*, *propellor* and *whirl* and *chamfer* give polyhedra that are not particularly smooth or convex, the faces may not be flat or symmetric. 35 | 36 | ![conway cgC](/images/conway_cgC.png) 37 | 38 | The canonical form of a convex polyhedra has all faces planar and all edges tangential to the unit sphere. The centre of gravity of the tangential points is also at the centre of the same unit sphere. 39 | 40 | The module *canon.py* contiains functions that attempt to shift the points of a polyhedron to satisfy these conditions. This is a iterative process and can take several hundred steps to converge. 41 | 42 | To try this in Sverchok, add the *canon.py* and *snl_canon.py* files as text blocks in your Blender file and add *snl_canon.py* as a *Scripted Node Lite*. The node has two parameters *iterations* and *scale_factor*. At each iteration the vertices are moved a *scale_factor* fraction of the calculated distance. Setting this parameter too high may cause the shape to become unstable. Increasing the *iterations* will increase the calculation time. 43 | 44 | ![conway_CcgC.png](/images/conway_CcgC.png) 45 | 46 | The canonicalization can also be applied after each operator. In the example below just enough iterations have been applied to form a pleasing shape. The proper canonical form of this polyhedra should be the same whether the canonicalization is performed once or twice. 47 | 48 | ![conway_CcCgC.png](/images/conway_CcCgC.png) 49 | 50 | These Conway operators can be applied to any manifold (ie. a closed solid) mesh not just the platonic solids. They currently don't work on planar grids unless one applies a solidify node to the grid first. 51 | 52 | ![conway_kg_hexa_grid](/images/conway_kg_hexa_grid.png) 53 | 54 | Other Sverchok nodes of course can be used interspersed with the Conway operators for other effects. 55 | 56 | I've only implemented a subset of the operators defined on the Wikipedia page. Many of the operators are equivalent to a combination of other operators as shown in the chart 57 | 58 | ### Conversion chart 59 | The operator order is given as the left to right node order. Note that this is the opposite to the order given in the Conway notation. 60 | 61 | | Operator | Description | Implementation | 62 | | :----------- | :------------ | :----------- | 63 | | kis | poke face | node | 64 | | dual | faces become vertices, vertices become faces | node | 65 | | ambo | full vertex bevel | node | 66 | | chamfer | hexagons replace edges | node | 67 | | gyro | faces divided into pentagons | node | 68 | | whirl | insets a smaller rotated copy of the face | node | 69 | | propellor | insets a rotated copy of the face | node | 70 | | zip | dual of kis | kis dual | 71 | | expand | edge bevel | ambo ambo | 72 | | bevel | vertex bevel applied twice | ambo dual kis dual | 73 | | snub | dual of gyro | gyro dual | 74 | | join | dual of ambo | ambo dual | 75 | | needle | dual of truncate | dual kis | 76 | | ortho | single subdivide | ambo ambo dual | 77 | | meta | poke face and subdivide edges | ambo dual kis | 78 | | truncate | half vertex bevel| dual kis dual | 79 | 80 | 81 | See my [Look Think Make](http://elfnor.com/) blog for more info. 82 | 83 | -------------------------------------------------------------------------------- /test_conway.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | test_conway.py 5 | 6 | tests for conway.py 7 | uses standalaone version of mathutils 8 | https://github.com/majimboo/py-mathutils 9 | 10 | Created on Sat Nov 11 15:03:24 2017 11 | 12 | @author: elfnor 13 | """ 14 | from collections import defaultdict 15 | import random 16 | import pytest 17 | import mathutils 18 | import conway 19 | 20 | from plato_solid import source as solid 21 | 22 | # ---- Face and edge functions 23 | 24 | 25 | def random_unit_vector(): 26 | """ 27 | points uniformly distributed on unit sphere 28 | http://mathworld.wolfram.com/SpherePointPicking.html 29 | """ 30 | x, y, z = [random.gauss(0.0, 1.0) for i in range(3)] 31 | r = (x * x + y * y + z * z)**0.5 32 | return mathutils.Vector([x/r, y/r, z/r]) 33 | 34 | 35 | def test_tri_face_center(): 36 | """ 37 | random triangular face with known center 38 | """ 39 | center_given = random_unit_vector() 40 | v1 = random_unit_vector() 41 | v2 = random_unit_vector() 42 | v3 = -1.0 * (v1 + v2) 43 | verts = [list(v1 + center_given), 44 | list(v2 + + center_given), 45 | list(v3 + center_given)] 46 | face = range(3) 47 | center_calc = conway.face_center(verts, face) 48 | assert list(center_given) == pytest.approx(center_calc, abs=1e-6) 49 | 50 | 51 | def test_simple_face_center(): 52 | """ 53 | square face at origin 54 | """ 55 | center_given = [0., 0., 0.] 56 | verts = [[1., 0., 0.], [0., 1., 0.], [-1., 0., 0.], [0., -1., 0.]] 57 | face = list(range(4)) 58 | center_calc = conway.face_center(verts, face) 59 | assert center_given == pytest.approx(center_calc) 60 | 61 | 62 | def test_simple_edge_center(): 63 | """ 64 | square face at origin 65 | """ 66 | verts = [[1., 1., 0.], [-1., 1., 0.], [-1., -1., 0.], [1., -1., 0.]] 67 | edge = [0, 1] 68 | center = conway.edge_center(verts, edge) 69 | assert center == pytest.approx([0., 1., 0.]) 70 | 71 | 72 | def test_simple_edge_third(): 73 | """ 74 | square face at origin 75 | """ 76 | verts = [[1., 1., 0.], [-1., 1., 0.], [-1., -1., 0.], [1., -1., 0.]] 77 | edge = [0, 1] 78 | third = conway.edge_third(verts, edge) 79 | assert third == pytest.approx([1/3., 1., 0.]) 80 | 81 | 82 | def test_simple_tangent_point(): 83 | """ 84 | quad face at origin 85 | """ 86 | verts = [[2., 1., 0.], [-1., 1., 0.], [-1., -1., 0.], [1., -1., 0.]] 87 | edge = [0, 1] 88 | tangent = conway.tangent_point(verts, edge) 89 | assert tangent == pytest.approx([0., 1., 0.]) 90 | 91 | # ---- flag tag functions 92 | 93 | 94 | def face_sort(faces_in): 95 | """ 96 | sorts each face so that lowest index is first but retaining face order 97 | then sorts all the faces 98 | used to compare equality of two face lists 99 | """ 100 | faces_out = [] 101 | for face in faces_in: 102 | argmin = face.index(min(face)) 103 | face_out = [] 104 | for v_ind, v1 in enumerate(face): 105 | face_out.insert(0, face[argmin - v_ind - 1]) 106 | faces_out.append(tuple(face_out)) 107 | 108 | return sorted(faces_out) 109 | 110 | 111 | @pytest.mark.parametrize("plato_type", ["4", "6", "8", "12", "20"]) 112 | def test_faces_flags(plato_type): 113 | """ 114 | convert to flags and back again 115 | should get same structure but different order or faces and verts within faces 116 | use face_sort to compare 117 | """ 118 | verts, faces1 = solid(plato_type) 119 | flags, vert_tags = conway.faces_to_flags(faces1) 120 | faces2, face_tags = conway.flags_to_faces(flags, vert_tags) 121 | assert face_sort(faces1) == face_sort(faces2) 122 | 123 | # ---- Conway Operators 124 | 125 | # test each for correct number of verts, edges, faces after operator 126 | 127 | 128 | def part_count(verts, faces): 129 | """ 130 | :param verts: list of x, y, z coords of verticies 131 | :param faces: list of indcies of verts in each face 132 | :return: count of verticices, edges and faces 133 | """ 134 | edge_count = 0.5 * len([v for face in faces for v in face]) 135 | vert_count = len(verts) 136 | face_count = len(faces) 137 | # check Euler characteristic 138 | assert vert_count + face_count - edge_count == 2 139 | return vert_count, edge_count, face_count 140 | 141 | 142 | @pytest.mark.parametrize("plato_type, count", [ 143 | ("4", (4, 6, 4)), 144 | ("6", (8, 12, 6)), 145 | ("8", (6, 12, 8)), 146 | ("12", (20, 30, 12)), 147 | ("20", (12, 30, 20)), 148 | ]) 149 | def test_part_count(plato_type, count): 150 | assert part_count(*solid(plato_type)) == count 151 | 152 | 153 | def check_mesh(verts, faces): 154 | """ 155 | :param verts: list of x, y, z coords of verticies 156 | :param faces: list of indcies of verts in each face 157 | :return: 158 | 159 | test verts and faces form a mesh as expected 160 | """ 161 | # face normal directions? 162 | # intersections and collisions? mathutils.geometry 163 | 164 | nverts = len(verts) 165 | face_count = defaultdict(int) 166 | for face in faces: 167 | # no duplicate verts in each face 168 | assert len(face) == len(set(face)) 169 | for v1, v2 in zip(face, face[1:] + face[:1]): 170 | # no vert indices in faces > len(verts) 171 | assert v1 < nverts 172 | edge_key = 'v{}v{}'.format(*sorted((v1, v2))) 173 | face_count[edge_key] += 1 174 | 175 | # every edge belongs to two and only two different faces 176 | assert list(face_count.values()) == [2] * len(face_count) 177 | 178 | # no duplicate faces 179 | assert len(faces) == len(set(face_sort(faces))) 180 | # 3 co-ords per vert 181 | for v_co in verts: 182 | assert len(v_co) == 3 183 | 184 | 185 | @pytest.mark.parametrize("plato_type", ["4", "6", "8", "12", "20"]) 186 | def test_mesh_plato(plato_type): 187 | check_mesh(*solid(plato_type)) 188 | 189 | 190 | # this fixture and test applies each of the conway operators to each of the 191 | # platonic solids, checks the v, e, f counts and mesh structure 192 | 193 | @pytest.mark.parametrize("plato_type", ["4", "6", "8", "12", "20"]) 194 | @pytest.mark.parametrize("cw_op, count_fns", [ 195 | (conway.kis, {'v': lambda v, e, f: v + f, 196 | 'e': lambda v, e, f: 3 * e, 197 | 'f': lambda v, e, f: 2 * e}), 198 | 199 | (conway.dual, {'v': lambda v, e, f: f, 200 | 'e': lambda v, e, f: e, 201 | 'f': lambda v, e, f: v}), 202 | 203 | (conway.ambo, {'v': lambda v, e, f: e, 204 | 'e': lambda v, e, f: 2 * e, 205 | 'f': lambda v, e, f: v + f}), 206 | 207 | (conway.chamfer, {'v': lambda v, e, f: v + 2 * e, 208 | 'e': lambda v, e, f: 4 * e, 209 | 'f': lambda v, e, f: e + f}), 210 | 211 | (conway.gyro, {'v': lambda v, e, f: v + 2 * e + f, 212 | 'e': lambda v, e, f: 5 * e, 213 | 'f': lambda v, e, f: 2 * e}), 214 | 215 | (conway.propellor, {'v': lambda v, e, f: v + 2 * e, 216 | 'e': lambda v, e, f: 5 * e, 217 | 'f': lambda v, e, f: 2 * e + f}), 218 | 219 | (conway.whirl, {'v': lambda v, e, f: v + 4 * e, 220 | 'e': lambda v, e, f: 7 * e, 221 | 'f': lambda v, e, f: 2 * e + f}), 222 | ]) 223 | def test_operator(plato_type, cw_op, count_fns): 224 | verts1, faces1 = solid(plato_type) 225 | v1, e1, f1 = part_count(verts1, faces1) 226 | verts2, faces2 = cw_op(verts1, faces1) 227 | check_mesh(verts2, faces2) 228 | v2, e2, f2 = part_count(verts2, faces2) 229 | assert v2 == count_fns['v'](v1, e1, f1) 230 | assert e2 == count_fns['e'](v1, e1, f1) 231 | assert f2 == count_fns['f'](v1, e1, f1) 232 | -------------------------------------------------------------------------------- /conway.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | functions to implement conway-hart operators on polyhedron 4 | designed for use with Svercook scripted nodes 5 | Is standalone apart from dependency on Blender mathutils module for vector functions 6 | 7 | """ 8 | from collections import defaultdict 9 | import mathutils 10 | 11 | 12 | # ---- Face and edge functions 13 | 14 | def face_center(verts, face, height=None): 15 | """ 16 | find the center of a face 17 | inputs: 18 | verts: list of x, y, z coords of verticies 19 | face: list of indcies of verts in face 20 | height: height of returned vertex above plane of face. 21 | output: 22 | center: center x, y, z coords as list 23 | """ 24 | verts_xyz = [verts[v_i] for v_i in face] 25 | x_co, y_co, z_co = zip(*verts_xyz) 26 | center = sum(x_co)/len(x_co), sum(y_co)/len(y_co), sum(z_co)/len(z_co) 27 | if height: 28 | center = mathutils.Vector(center) 29 | norm = mathutils.geometry.normal(verts_xyz) 30 | center = list(center + norm * height) 31 | return center 32 | 33 | 34 | def face_normal(verts, face): 35 | """ 36 | normal direction of face as mathutils Vector 37 | """ 38 | norm = mathutils.geometry.normal([verts[v_i] for v_i in face]) 39 | return norm 40 | 41 | 42 | def edge_center(verts, edge): 43 | """ 44 | find the middle point on edge 45 | """ 46 | v1_xyz = mathutils.Vector(verts[edge[0]]) 47 | v2_xyz = mathutils.Vector(verts[edge[1]]) 48 | va_xyz = v1_xyz + (v2_xyz - v1_xyz)/2.0 49 | return list(va_xyz) 50 | 51 | 52 | def edge_third(verts, edge): 53 | """ 54 | find a point one third along edge 55 | """ 56 | v1_xyz = mathutils.Vector(verts[edge[0]]) 57 | v2_xyz = mathutils.Vector(verts[edge[1]]) 58 | va_xyz = v1_xyz + (v2_xyz - v1_xyz)/3.0 59 | return list(va_xyz) 60 | 61 | 62 | def tangent_point(verts, edge): 63 | """ 64 | find the closest point to the origin on edge 65 | """ 66 | v1_xyz = mathutils.Vector(verts[edge[0]]) 67 | v2_xyz = mathutils.Vector(verts[edge[1]]) 68 | va_xyz, _s = mathutils.geometry.intersect_point_line(mathutils.Vector(), 69 | v1_xyz, v2_xyz) 70 | return list(va_xyz) 71 | 72 | 73 | # ---- flag tag functions 74 | 75 | def faces_to_flags(faces, face_tags=None, edge_key=False): 76 | """ 77 | takes a list of faces, where each face is given as a list of verts in CCW order 78 | and returns flags, verts_tags 79 | 80 | face_tags is an optional list of tags for the faces 81 | 82 | flags is a dict where the 83 | key face tag + vert tag 84 | value (face tag, vert tag, tag of next CCW vert in the face) 85 | 86 | if edge_key == True 87 | the key for flags is 88 | key : v1_t + v2_t 89 | this allows faces opposing across an edge to be found easily 90 | 91 | 92 | vert_tags 93 | key vert tag 94 | value index of vert 95 | 96 | 97 | This makes looking up the next vertex quick. There will be two flags for every edge in 98 | the mesh represented by faces. 99 | """ 100 | flags = {} 101 | vert_tags = {} 102 | for face_i, face in enumerate(faces): 103 | try: 104 | face_t = face_tags[face_i] 105 | except (TypeError, IndexError): 106 | face_t = 'f{}'.format(face_i) 107 | for v1, v2 in zip(face, face[1:] + face[:1]): 108 | v1_t = 'v{}'.format(v1) 109 | v2_t = 'v{}'.format(v2) 110 | if edge_key: 111 | flags[v1_t + v2_t] = [face_t, v1_t, v2_t] 112 | else: 113 | # default 114 | flags[face_t + v1_t] = [face_t, v1_t, v2_t] 115 | vert_tags[v1_t] = v1 116 | return flags, vert_tags 117 | 118 | 119 | def flags_to_faces(flags, vert_tags): 120 | """ 121 | flags is a dict where the 122 | key face tag + vert tag 123 | value (face tag, vert tag, tag of next CCW vert in the face) 124 | 125 | vert_tags 126 | key vert tag 127 | value index of vert 128 | 129 | returns a list of faces, where each face is 130 | given as a list of vert indices in CCW order 131 | and a list of face tags in the same order as faces 132 | """ 133 | face_count = defaultdict(int) 134 | vert_one = {} 135 | faces = [] 136 | face_tags = [] # list of face_t in the same order as faces 137 | 138 | # find how many verts in each face, and keep one vert from each face 139 | for flag in flags.values(): 140 | face_count[flag[0]] += 1 141 | vert_one[flag[0]] = flag[1] 142 | 143 | for face_t, vert_count in face_count.items(): 144 | face_vt = [vert_one[face_t]] 145 | while len(face_vt) < vert_count: 146 | v1_t = face_vt[-1] 147 | # find the flag (face_t, v1_t, v2_t) 148 | v2_t = flags[face_t + v1_t][2] 149 | face_vt.append(v2_t) 150 | face_ccw = [vert_tags[v_t] for v_t in face_vt] 151 | faces.append(face_ccw) 152 | face_tags.append(face_t) 153 | return faces, face_tags 154 | 155 | 156 | def face_vt_to_flags(face_t, face_vt): 157 | """ 158 | returns the flags for a complete new face 159 | input: 160 | face_t: string tag to name face 161 | face_vt: list of vertex tags 162 | output 163 | flags: flags is a dict where the 164 | key face tag + vert tag 165 | value (face tag, vert tag, tag of next CCW vert in the face) 166 | """ 167 | flags = {} 168 | for v1_t, v2_t in zip(face_vt, face_vt[1:] + face_vt[:1]): 169 | flags[face_t + v1_t] = [face_t, v1_t, v2_t] 170 | 171 | return flags 172 | 173 | 174 | # ---- Conway Operators 175 | 176 | def kis(verts_in, faces_in, height=0.0): 177 | """ 178 | each n-face is divided into n triangles which extend to the face centroid 179 | existing vertices retained 180 | equivalent to Blender poke operator 181 | """ 182 | flags_kis = {} 183 | verts_kis = verts_in[:] 184 | vert_tags = {'v{}'.format(i): i for i, v in enumerate(verts_kis)} 185 | 186 | for face_i, face in enumerate(faces_in): 187 | verts_kis.append(face_center(verts_in, face, height)) 188 | vert_tags['vf{}'.format(face_i)] = len(verts_kis) - 1 189 | for v1, v2 in zip(face, face[1:] + face[:1]): 190 | # 3 flags for the face 191 | f3_t = 'f{}:{}'.format(face_i, v1) 192 | va_t = 'v{}'.format(v1) 193 | vb_t = 'v{}'.format(v2) 194 | vc_t = 'vf{}'.format(face_i) 195 | flags_new = face_vt_to_flags(f3_t, [va_t, vb_t, vc_t]) 196 | flags_kis.update(flags_new) 197 | 198 | faces_kis = flags_to_faces(flags_kis, vert_tags)[0] 199 | return verts_kis, faces_kis 200 | 201 | 202 | def dual(verts_in, faces_in): 203 | """ 204 | faces become vertices, vertices become faces 205 | The dual of a polyhedron is another mesh wherein: 206 | - every face in the original becomes a vertex in the dual 207 | - every vertex in the original becomes a face in the dual 208 | v = f, e = e, f = v 209 | 210 | """ 211 | verts_dual = [] 212 | verts_out_tags = {} 213 | flags_dual = {} 214 | 215 | # make edge indexed flags for old mesh 216 | flags_in, verts_in_tags = faces_to_flags(faces_in, edge_key=True) 217 | 218 | for face_i, face in enumerate(faces_in): 219 | verts_dual.append(face_center(verts_in, face)) 220 | verts_out_tags['vf{}'.format(face_i)] = len(verts_dual) - 1 221 | 222 | for v1, v2 in zip(face, face[1:] + face[:1]): 223 | va_t = 'vf{}'.format(face_i) 224 | # find the tag of the face across edge (v1, v2) from face_i 225 | edge_key = 'v{}v{}'.format(v2, v1) 226 | fopp_t = flags_in[edge_key][0] 227 | vb_t = fopp_t.replace('f', 'vf') 228 | 229 | # make one new flag for each old half-edge 230 | fdual_t = 'fv{}'.format(v1) 231 | flags_dual[fdual_t + vb_t] = [fdual_t, vb_t, va_t] 232 | 233 | faces, face_tags = flags_to_faces(flags_dual, verts_out_tags) 234 | 235 | # sort outgoing faces to be in same order as incoming verts 236 | face_dict = {int(k[2:]): v for k, v in zip(face_tags, faces)} 237 | faces_sorted = [face_dict[i] for i in sorted(face_dict)] 238 | 239 | return verts_dual, faces_sorted 240 | 241 | 242 | def ambo(verts_in, faces_in): 243 | """ 244 | New vertices are added mid-edges, while old vertices are removed. 245 | results in a face per original face and a face per original vertex 246 | This is full truncation to the mid-point of the edge 247 | equivalent to the bevel operator, vertex only, percent, amount = 50 2e 248 | """ 249 | verts_ambo = [] # new verts at the centre of old edges 250 | vert_tags = {} 251 | flags_ambo = {} 252 | 253 | for face_i, face in enumerate(faces_in): 254 | for v1, v2, v3 in zip(face, face[1:] + face[:1], face[2:] + face[:2]): 255 | va_t = 'v{}:{}'.format(*sorted((v1, v2))) 256 | if va_t not in vert_tags: 257 | verts_ambo.append(edge_center(verts_in, (v1, v2))) 258 | vert_tags[va_t] = len(verts_ambo) - 1 259 | 260 | vb_t = 'v{}:{}'.format(*sorted((v2, v3))) 261 | # add two flags along the edge (va, vb) 262 | fcenter_t = 'f{}'.format(face_i) 263 | flags_ambo[fcenter_t + va_t] = [fcenter_t, va_t, vb_t] 264 | fvert_t = 'fv{}'.format(v2) 265 | flags_ambo[fvert_t + vb_t] = [fvert_t, vb_t, va_t] 266 | 267 | faces_ambo = flags_to_faces(flags_ambo, vert_tags)[0] 268 | return verts_ambo, faces_ambo 269 | 270 | 271 | def chamfer(verts_in, faces_in, thickness=0.1, height=0.1): 272 | """ 273 | An edge-truncation. 274 | New hexagonal faces are added in place of edges. 275 | v = v + 2e, e = 4e, f = f + e 276 | """ 277 | flags_chamf = {} 278 | 279 | verts_chamf = verts_in[:] 280 | vert_tags = {'v{}'.format(i): i for i, v in enumerate(verts_chamf)} 281 | 282 | for face_i, face in enumerate(faces_in): 283 | for v1, v2 in zip(face, face[1:] + face[:1]): 284 | center = mathutils.Vector(face_center(verts_chamf, face)) 285 | v2_xyz = mathutils.Vector(verts_chamf[v2]) 286 | 287 | face_norm = mathutils.geometry.normal([verts_chamf[v_i] for v_i in face]) 288 | vb_xyz = v2_xyz + (center - v2_xyz) * thickness + face_norm * height 289 | 290 | verts_chamf.append(list(vb_xyz)) 291 | vb_t = 'v{}f{}'.format(v2, face_i) 292 | vert_tags[vb_t] = len(verts_chamf) - 1 293 | 294 | # add 4 flags 295 | face_chamf_t = 'f{}:{}'.format(*sorted((v1, v2))) 296 | face_t = 'f{}'.format(face_i) 297 | va_t = 'v{}'.format(v2) 298 | vc_t = 'v{}f{}'.format(v1, face_i) 299 | vd_t = 'v{}'.format(v1) 300 | 301 | flags_chamf[face_chamf_t + va_t] = [face_chamf_t, va_t, vb_t] 302 | flags_chamf[face_chamf_t + vb_t] = [face_chamf_t, vb_t, vc_t] 303 | flags_chamf[face_chamf_t + vc_t] = [face_chamf_t, vc_t, vd_t] 304 | 305 | flags_chamf[face_t + vc_t] = [face_t, vc_t, vb_t] 306 | 307 | faces_chamf = flags_to_faces(flags_chamf, vert_tags)[0] 308 | return verts_chamf, faces_chamf 309 | 310 | 311 | def gyro(verts_in, faces_in): 312 | """ 313 | gyro is like kis but with the new edges connecting the face centers to the 1/3 points 314 | on the edges rather than the vertices. 315 | v = v + 2e + f, f = 2e , e = 5e 316 | """ 317 | verts_gyro = verts_in[:] 318 | vert_tags = {'v{}'.format(i): i for i, v in enumerate(verts_gyro)} 319 | 320 | flags_gyro = {} 321 | 322 | for face_i, face in enumerate(faces_in): 323 | verts_gyro.append(face_center(verts_in, face)) 324 | vert_tags['vf{}'.format(face_i)] = len(verts_gyro) - 1 325 | 326 | for v1, v2, v3 in zip(face, face[1:] + face[:1], face[2:] + face[:2]): 327 | va_t = 'v{}:{}'.format(v1, v2) 328 | verts_gyro.append(edge_third(verts_in, (v1, v2))) 329 | vert_tags[va_t] = len(verts_gyro) - 1 330 | 331 | # do five flags for the new face that shares two egdes with edge v1, v2 332 | face_t = 'f{}:{}'.format(face_i, v2) 333 | vb_t = 'v{}:{}'.format(v2, v1) 334 | vc_t = 'v{}'.format(v2) 335 | vd_t = 'v{}:{}'.format(v2, v3) 336 | ve_t = 'vf{}'.format(face_i) 337 | 338 | flags_new = face_vt_to_flags(face_t, [va_t, vb_t, vc_t, vd_t, ve_t]) 339 | flags_gyro.update(flags_new) 340 | 341 | faces_gyro = flags_to_faces(flags_gyro, vert_tags)[0] 342 | return verts_gyro, faces_gyro 343 | 344 | 345 | def propellor(verts_in, faces_in): 346 | """ 347 | builds a new 'skew face' by making new points along edges, 1/3rd the distance from v1->v2, 348 | then connecting these into a new inset face. This breaks rotational symmetry about the 349 | faces, whirling them into gyres 350 | v = v +2e, e = 5e, f = f + 2e 351 | """ 352 | verts_prop = verts_in[:] 353 | vert_tags = {'v' + str(i): i for i, v in enumerate(verts_prop)} 354 | 355 | flags_prop = {} 356 | 357 | for face_i, face in enumerate(faces_in): 358 | for v1, v2, v3 in zip(face, face[1:] + face[:1], face[2:] + face[:2]): 359 | va_t = 'v{}:{}'.format(v1, v2) 360 | verts_prop.append(edge_third(verts_in, (v1, v2))) 361 | vert_tags[va_t] = len(verts_prop) - 1 362 | 363 | face_t = 'f{}'.format(face_i) 364 | f4_t = 'f{}:{}'.format(face_i, v2) 365 | vb_t = 'v{}:{}'.format(v2, v1) 366 | vc_t = 'v{}'.format(v2) 367 | vd_t = 'v{}:{}'.format(v2, v3) 368 | 369 | # add flag for centre face 370 | flags_prop[face_t + va_t] = [face_t, va_t, vd_t] 371 | # add 4 sided face which has two verts (vA, vB) along edge v1, v2 372 | flags_new = face_vt_to_flags(f4_t, [va_t, vb_t, vc_t, vd_t]) 373 | flags_prop.update(flags_new) 374 | 375 | faces_prop = flags_to_faces(flags_prop, vert_tags)[0] 376 | return verts_prop, faces_prop 377 | 378 | 379 | def whirl(verts_in, faces_in): 380 | """ 381 | gyro followed by truncation of vertices centered at original faces. 382 | This create 2 new hexagons for every original edge, 383 | v = v+4e, e=7e, f=f+2e 384 | """ 385 | verts_whirl = verts_in[:] 386 | vert_tags = {'v{}'.format(i): i for i, v in enumerate(verts_whirl)} 387 | 388 | flags_whirl = {} 389 | 390 | for face_i, face in enumerate(faces_in): 391 | 392 | for v1, v2, v3 in zip(face, face[1:] + face[:1], face[2:] + face[:2]): 393 | # new vert on face 394 | va_t = 'vf{}:{}'.format(face_i, v1) 395 | center = mathutils.Vector(face_center(verts_whirl, face)) 396 | v1_xyz = mathutils.Vector(verts_whirl[v1]) 397 | va_xyz = v1_xyz + (center - v1_xyz)/2.0 398 | verts_whirl.append(list(va_xyz)) 399 | vert_tags[va_t] = len(verts_whirl) - 1 400 | 401 | # new vert on edge 402 | vb_t = 'v{}:{}'.format(v1, v2) 403 | verts_whirl.append(edge_third(verts_in, (v1, v2))) 404 | vert_tags[vb_t] = len(verts_whirl) - 1 405 | 406 | # 7 flags 407 | f6_t = 'f{}:{}'.format(face_i, v2) 408 | 409 | vc_t = 'v{}:{}'.format(v2, v1) 410 | vd_t = 'v{}'.format(v2) 411 | ve_t = 'v{}:{}'.format(v2, v3) 412 | vf_t = 'vf{}:{}'.format(face_i, v2) 413 | 414 | flags_new = face_vt_to_flags(f6_t, 415 | [va_t, vb_t, vc_t, vd_t, ve_t, vf_t]) 416 | flags_whirl.update(flags_new) 417 | 418 | face_t = 'f{}'.format(face_i) 419 | flags_whirl[face_t + va_t] = [face_t, va_t, vf_t] 420 | 421 | faces_whirl = flags_to_faces(flags_whirl, vert_tags)[0] 422 | return verts_whirl, faces_whirl 423 | --------------------------------------------------------------------------------