└── RestoreSymmetry ├── README.md └── mesh_restoresymmetry.py /RestoreSymmetry/README.md: -------------------------------------------------------------------------------- 1 | # Restore Symmetry 2 | 3 | Restore Symmetry is a fork of Remirror, an addon that was developed by Philip Lafleur and posted on the Blender wiki and tracker. (https://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Mesh/Remirror) 4 | 5 | Its purpose is to reestablish symmetry in a mirrored mesh after sculpting (which does not keep perfect symmetry in all cases) or other mesh editing that disrupts symmetry. 6 | 7 | I edited the addon to add support for restoring symmetry of shape keys. 8 | 9 | ## Installing 10 | 11 | Download the .py file by either clicking on it and then clicking RAW, or download the whole master branch with all addons (currently only this one exists). Extract to your addon folder. 12 | 13 | 14 | ## Usage 15 | 16 | Please read [https://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Mesh/Remirror](https://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Mesh/Remirror) to see how it works. 17 | 18 | It's pretty simple though, just select a mesh in object mode and do Object - Mirror - Restore Symmetry or just Restore Symmetry from the spacebar search menu. It will fail if your mesh doesn't have a loop through the middle of the mesh. 19 | 20 | If you want to add a hotkey you can add it in 3dview - Object mode and the command is: 21 | ``` 22 | mesh.restoresymmetry 23 | ``` 24 | 25 | ## Authors 26 | 27 | * **Philip Lafleur** - *Original creator of the Remirror addon* 28 | * **Henrik Berglund** - *Edits to the addon* 29 | * **Sergey Meshkov** - *Edits to the addon* 30 | ## License 31 | 32 | GPL 33 | 34 | ## Acknowledgements 35 | 36 | Thanks to the original author Philip Lafleur for the original Remirror addon. 37 | -------------------------------------------------------------------------------- /RestoreSymmetry/mesh_restoresymmetry.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 2 | 3 | # ***** BEGIN GPL LICENSE BLOCK ***** 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # The Original Code is Copyright (C) 2012 by Philip Lafleur. 19 | # All rights reserved. 20 | # 21 | # Contact: bksnzq {at} gmail {dot} com 22 | # 23 | # The Original Code is: all of this file. 24 | # 25 | # Contributor(s): none yet. 26 | # 27 | # ***** END GPL LICENSE BLOCK ***** 28 | 29 | bl_info = { 30 | "name": "Restore Symmetry (originally Remirror)", 31 | "author": "Philip Lafleur (original author), Henrik Berglund (edits), Sergey Meshkov (edits)", 32 | "version": (1, 0, 3), 33 | "blender": (2, 80, 0), 34 | "location": "View3D > Object > Mirror > Restore Symmetry", 35 | "description": "Non-destructively update symmetry of a mirrored mesh (and shapekeys)", 36 | "warning": "", 37 | "wiki_url": ("Original wiki: http://wiki.blender.org/index.php/Extensions:2.6" 38 | "/Py/Scripts/Mesh/Remirror"), 39 | "tracker_url": ("Original tracker post (now archived): http://projects.blender.org/tracker/index.php?" 40 | "func=detail&aid=32166&group_id=153&atid=467"), 41 | "category": "Mesh"} 42 | 43 | import bpy 44 | import bmesh 45 | 46 | ERR_ASYMMETRY = "Asymmetry encountered (central edge loop(s) not centered?)" 47 | ERR_BAD_PATH = "Couldn't follow edge path (inconsistent normals?)" 48 | ERR_CENTRAL_LOOP = "Failed to find central edge loop(s). Please recenter." 49 | ERR_FACE_COUNT = "Encountered edge with more than 2 faces attached." 50 | 51 | CENTRAL_LOOP_MARGIN = 1e-5 52 | 53 | 54 | class RestoreSymmetry_OT_RestoreSymmetry(bpy.types.Operator): 55 | bl_idname = "mesh.restoresymmetry" 56 | bl_label = "Restore Symmetry" 57 | bl_description = "Non-destructively update symmetry of a mirrored mesh (and shapekeys)" 58 | bl_options = {'REGISTER', 'UNDO'} 59 | 60 | axis: bpy.props.EnumProperty( 61 | name = "Axis", 62 | description = "Mirror axis", 63 | default = 'X', 64 | items = (('X', "X", "X Axis"), 65 | ('Y', "Y", "Y Axis"), 66 | ('Z', "Z", "Z Axis"))) 67 | source: bpy.props.EnumProperty( 68 | name = "Source", 69 | description = "Which half of the mesh to use as mirror source", 70 | default = 'POSITIVE', 71 | items = (('POSITIVE', "Positive side", "Positive side"), 72 | ('NEGATIVE', "Negative side", "Negative side"))) 73 | targetmix: bpy.props.FloatProperty( 74 | name = "Target Mix Amount", 75 | description = "How much target coordinates should contribute", 76 | default = 0.0, min = 0.0, max = 1.0) 77 | 78 | @classmethod 79 | def poll (cls, context): 80 | obj = context.active_object 81 | return obj and obj.type == 'MESH' and context.mode == 'OBJECT' or 'EDIT_MESH' or 'SCULPT' 82 | 83 | def execute(self, context): 84 | mesh = bpy.context.active_object.data 85 | mode = bpy.context.mode 86 | 87 | if bpy.context.active_object.data.shape_keys is None: 88 | shapekey = None 89 | else: 90 | shapekey = bpy.context.active_object.active_shape_key.name #if shapekey was found add to shapekey for later 91 | 92 | bpy.ops.object.mode_set(mode='OBJECT') #go to object mode for bmesh operation 93 | 94 | try: 95 | restore_symmetry(mesh, shapekey, {'X': 0, 'Y': 1, 'Z': 2}[self.axis], self.source, self.targetmix) 96 | except ValueError as e: 97 | self.report ({'ERROR'}, str(e)) 98 | 99 | if mode == 'EDIT_MESH': 100 | bpy.ops.object.mode_set(mode='EDIT') #return to edit mode 101 | if mode == 'SCULPT': 102 | bpy.ops.object.mode_set(mode='SCULPT') # return to sculpt mode 103 | 104 | return {'FINISHED'} 105 | 106 | 107 | def next_edgeCCW(v, e_prev): 108 | """ 109 | Return the edge following e_prev in counter-clockwise order around vertex v 110 | by following the winding of the surrounding faces. 111 | """ 112 | if len(e_prev.link_loops) == 2: 113 | # Assumes continuous normals 114 | if e_prev.link_loops[0].vert is v: 115 | return e_prev.link_loops[0].link_loop_prev.edge 116 | return e_prev.link_loops[1].link_loop_prev.edge 117 | 118 | elif len(e_prev.link_loops) == 1: 119 | # Assumes only two single-loop edges per vertex 120 | if e_prev.link_loops[0].vert is v: 121 | return e_prev.link_loops[0].link_loop_prev.edge 122 | for edge in v.link_edges: 123 | if len(edge.link_loops) == 1 and edge is not e_prev: 124 | return edge 125 | 126 | else: 127 | raise ValueError(ERR_FACE_COUNT) 128 | 129 | 130 | def next_edge_CW(v, e_prev): 131 | """ 132 | Return the edge following e_prev in clockwise order around vertex v 133 | by following the winding of the surrounding faces. 134 | """ 135 | if len(e_prev.link_loops) == 2: 136 | # Assumes continuous normals 137 | if e_prev.link_loops[0].vert is not v: 138 | return e_prev.link_loops[0].link_loop_next.edge 139 | return e_prev.link_loops[1].link_loop_next.edge 140 | 141 | elif len(e_prev.link_loops) == 1: 142 | # Assumes only two single-loop edges per vertex 143 | if e_prev.link_loops[0].vert is not v: 144 | return e_prev.link_loops[0].link_loop_next.edge 145 | for edge in v.link_edges: 146 | if len (edge.link_loops) == 1 and edge is not e_prev: 147 | return edge 148 | 149 | else: 150 | raise ValueError (ERR_FACE_COUNT) 151 | 152 | 153 | def visit_mirror_verts(v_start, e_start, visitor, shapelayer, shapekey): 154 | """ 155 | Call visitor(v_right, v_left) for each pair of mirrored vertices that 156 | are reachable by following a path from v_start along connected edges 157 | without intersecting the central edge loop(s) or any previously-visited 158 | vertices. 159 | 160 | v_start: a vertex on a central edge loop 161 | e_start: an edge on a central edge loop such that the next edge in 162 | counter-clockwise order around v_start is on the positive side 163 | of the central loop 164 | """ 165 | er = e_start 166 | el = e_start 167 | vr = v_start 168 | vl = v_start 169 | path = [(er, el)] 170 | 171 | while path: 172 | er = next_edgeCCW(vr, er) 173 | el = next_edge_CW(vl, el) 174 | 175 | if er is path[-1][0] or er.tag: 176 | if not (el is path[-1][1] or el.tag): 177 | raise ValueError(ERR_ASYMMETRY) 178 | er = path[-1][0] 179 | el = path[-1][1] 180 | vr = er.other_vert(vr) 181 | vl = el.other_vert(vl) 182 | path.pop() 183 | continue 184 | 185 | if el is path[-1][1] or el.tag: 186 | raise ValueError(ERR_ASYMMETRY) 187 | 188 | vr = er.other_vert(vr) 189 | vl = el.other_vert(vl) 190 | 191 | if vr is None: 192 | raise ValueError(ERR_BAD_PATH) 193 | if vr.tag: 194 | if vl is None or not vl.tag: 195 | raise ValueError(ERR_ASYMMETRY) 196 | vr = er.other_vert(vr) 197 | vl = el.other_vert(vl) 198 | continue 199 | 200 | if vl is None or vl.tag: 201 | raise ValueError(ERR_ASYMMETRY) 202 | 203 | path.append((er, el)) 204 | visitor(vr, vl) 205 | vr.tag = True 206 | vl.tag = True 207 | 208 | 209 | def update_verts(v_start, e_start, axis, source, shapelayer, shapekey, targetmix): 210 | def update_positive(v_right, v_left): 211 | if(shapekey=="Basis" or shapekey == None): #no shapekeys or basis shapekey selected - use original code 212 | # mix source and target (default mix amount 0 means use 100% source); mix at target, then update source 213 | v_left.co = targetmix*v_left.co + (1.0-targetmix)*v_right.co 214 | v_left.co[axis] = v_left.co[axis] - 2.0*(1.0-targetmix)*v_right.co[axis] 215 | v_right.co = v_left.co 216 | v_right.co[axis] = -v_left.co[axis] 217 | else: #shapekeys found - use edited code 218 | # mix source and target (default mix amount 0 means use 100% source); mix at target, then update source 219 | v_left[shapelayer] = targetmix*v_left[shapelayer] + (1.0-targetmix)*v_right[shapelayer] 220 | v_left[shapelayer][axis] = v_left[shapelayer][axis] - 2.0*(1.0-targetmix)*v_right[shapelayer][axis] 221 | v_right[shapelayer] = v_left[shapelayer] 222 | v_right[shapelayer][axis] = -v_left[shapelayer][axis] 223 | 224 | def update_negative(v_right, v_left): 225 | if(shapekey=="Basis" or shapekey == None): #no shapekeys or basis shapekey selected - use original code 226 | # mix source and target (default mix amount 0 means use 100% source); mix at target, then update source 227 | v_right.co = targetmix*v_right.co + (1.0-targetmix)*v_left.co 228 | v_right.co[axis] = v_right.co[axis] - 2.0*(1.0-targetmix)*v_left.co[axis] 229 | v_left.co = v_right.co 230 | v_left.co[axis] = -v_right.co[axis] 231 | else: #shapekeys found - use edited code 232 | # mix source and target (default mix amount 0 means use 100% source); mix at target, then update source 233 | v_right[shapelayer] = targetmix*v_right[shapelayer] + (1.0-targetmix)*v_left[shapelayer] 234 | v_right[shapelayer][axis] = v_right[shapelayer][axis] - 2.0*(1.0-targetmix)*v_left[shapelayer][axis] 235 | v_left[shapelayer] = v_right[shapelayer] 236 | v_left[shapelayer][axis] = -v_right[shapelayer][axis] 237 | 238 | visit_mirror_verts( 239 | v_start, e_start, 240 | update_positive if source == 'POSITIVE' else update_negative, shapelayer, shapekey) 241 | 242 | 243 | def tag_central_edge_path(v, e): 244 | """ 245 | Tag each edge along the path starting at edge e in the direction of vertex v 246 | such that the path evenly divides the number of edges connected to each 247 | vertex. 248 | """ 249 | while True: 250 | e.tag = True 251 | 252 | if len(v.link_edges) % 2: 253 | if len(v.link_faces) == len(v.link_edges): 254 | raise ValueError(ERR_CENTRAL_LOOP) 255 | else: 256 | return 257 | 258 | for i in range(len(v.link_edges) // 2): 259 | e = next_edgeCCW(v, e) 260 | 261 | v = e.other_vert(v) 262 | if v is None: 263 | raise ValueError(ERR_BAD_PATH) 264 | 265 | if e.tag: 266 | return 267 | 268 | 269 | def tag_central_loops (bm, axis): 270 | """ 271 | Attempt to find and tag the edges on the central loop(s) of the bmesh bm 272 | aligned with the given axis. 273 | """ 274 | for v in bm.verts: 275 | v.tag = False 276 | for e in bm.edges: 277 | e.tag = False 278 | 279 | verts = [] 280 | edges = [] 281 | 282 | for v in bm.verts: 283 | if CENTRAL_LOOP_MARGIN > v.co[axis] > -CENTRAL_LOOP_MARGIN: 284 | v.tag = True 285 | verts.append(v) 286 | 287 | for v in verts: 288 | for e in v.link_edges: 289 | if e.other_vert(v).tag: 290 | e.tag = True 291 | edges.append(e) 292 | 293 | for v in verts: 294 | v.tag = False 295 | 296 | if not(edges and verts): 297 | raise ValueError(ERR_CENTRAL_LOOP) 298 | 299 | for e in edges: 300 | tag_central_edge_path(e.verts[0], e) 301 | tag_central_edge_path(e.verts[1], e) 302 | 303 | 304 | def starting_vertex(edge, axis): 305 | """ 306 | Return the endpoint of the given edge such that the next edge in 307 | counter-clockwise order around the endpoint is on the positive side of 308 | the given axis. 309 | """ 310 | if len(edge.link_loops) != 2: 311 | raise ValueError(ERR_FACE_COUNT) 312 | 313 | loops = sorted(edge.link_loops, 314 | key = lambda loop: loop.face.calc_center_median()[axis]) 315 | 316 | return loops[-1].vert 317 | 318 | 319 | def restore_symmetry(mesh, shapekey, axis, source, targetmix): 320 | bm = bmesh.new () 321 | bm.from_mesh(mesh) 322 | 323 | if shapekey is None: 324 | shapelayer = None 325 | else: 326 | shapelayer = bm.verts.layers.shape[shapekey] #if the mesh had shapekeys, set the BM layer for the shapekey 327 | 328 | tag_central_loops(bm, axis) 329 | 330 | for e in bm.edges: 331 | if e.tag: 332 | e.verts[0].co[axis] = 0. 333 | e.verts[1].co[axis] = 0. 334 | e.verts[0].tag = True 335 | e.verts[1].tag = True 336 | 337 | for e in bm.edges: 338 | if e.tag: 339 | update_verts(starting_vertex(e, axis), e, axis, source, shapelayer, shapekey, targetmix) 340 | 341 | for v in bm.verts: 342 | v.tag = False 343 | for e in bm.edges: 344 | e.tag = False 345 | 346 | bm.to_mesh(mesh) 347 | mesh.update() 348 | 349 | 350 | def menufunc(self, context): 351 | self.layout.operator(RestoreSymmetry_OT_RestoreSymmetry.bl_idname) 352 | 353 | 354 | def register(): 355 | bpy.utils.register_class(RestoreSymmetry_OT_RestoreSymmetry) 356 | bpy.types.VIEW3D_MT_mirror.append(menufunc) 357 | 358 | 359 | def unregister(): 360 | bpy.types.VIEW3D_MT_mirror.remove(menufunc) 361 | bpy.utils.unregister_class(RestoreSymmetry_OT_RestoreSymmetry) 362 | 363 | 364 | if __name__ == "__main__": 365 | register() 366 | --------------------------------------------------------------------------------