├── .gitignore ├── README.md ├── __init__.py ├── arcadjust.py ├── blender_manifest.toml ├── build_extension.bat ├── connect_edges.py ├── context_bevel.py ├── copypaste.py ├── createtube.py ├── cursor.py ├── dimensions.py ├── edgeweight.py ├── extend.py ├── extrudealongpath.py ├── grabapplymat.py ├── knifescreen.py ├── linear_deformer.py ├── loopring.py ├── move_to_furthest.py ├── naming.py ├── panel.py ├── polypatch.py ├── preferences.py ├── propertygroup.py ├── quickboolean.py ├── quickmaterial.py ├── radial_align.py ├── reduce.py ├── screenreflect.py ├── selectionmode.py ├── targetweld.py ├── thicken.py ├── vnormals.py └── workplane.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.zip 3 | *.mdmp 4 | *.whl -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rmKit 2 | 3 | **rmKit** is a feature-rich Blender addon designed to enhance your 3D modeling workflow with a wide array of tools and utilities. It provides advanced functionality for mesh editing, precision modeling, and material management, making it an essential toolkit for both professionals and hobbyists. 4 | 5 | --- 6 | 7 | ## Key Features 8 | 9 | - **Advanced Mesh Editing Tools**: Includes operators for connecting edges, creating tubes, extruding along edges, thickening meshes, and much more. 10 | - **Customizable Workflow**: Tools like quick material assignment, radial alignment, and workplane setup allow for a highly adaptable workflow. 11 | - **Precision Modeling**: Features like the dimensions visualizer, linear falloff tool, move-to-furthest tool, and knife screen ensure accuracy in your designs. 12 | - **Selection-Based Vertex Normals**: A custom tool for managing vertex normals based on selection sets, enabling fine-tuned control over shading. 13 | - **User-Friendly Panels**: Intuitive UI panels for easy access to tools and settings, organized for efficient navigation. 14 | 15 | --- 16 | 17 | ## Why Use rmKit? 18 | 19 | - **Time-Saving**: Automates repetitive tasks and simplifies complex operations. 20 | - **Versatile**: Suitable for a wide range of modeling tasks, from hard-surface modeling to organic shapes. 21 | - **Integrated**: Seamlessly integrates into Blender's interface for a smooth user experience. 22 | - **Customizable**: Offers options to tailor tools to your specific needs. 23 | 24 | --- 25 | 26 | ## Get Started 27 | 28 | Enable rmKit in Blender's Add-ons menu and explore its extensive set of tools to supercharge your modeling workflow. Whether you're creating intricate designs or managing large projects, rmKit is your go-to addon for efficiency and precision. 29 | 30 | For more information, visit the [official documentation](https://rmkit.readthedocs.io/en/latest/). -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "rmKit", 3 | "author": "Timothee Yeramian", 4 | "location": "View3D > Sidebar", 5 | "description": "Collection of Tools", 6 | "category": "", 7 | "blender": ( 3, 3, 1), 8 | "warning": "", 9 | "doc_url": "https://rmkit.readthedocs.io/en/latest/", 10 | } 11 | 12 | import bpy 13 | 14 | from . import ( 15 | propertygroup, 16 | polypatch, 17 | reduce, 18 | context_bevel, 19 | loopring, 20 | move_to_furthest, 21 | knifescreen, 22 | extrudealongpath, 23 | connect_edges, 24 | arcadjust, 25 | targetweld, 26 | createtube, 27 | vnormals, 28 | copypaste, 29 | cursor, 30 | workplane, 31 | screenreflect, 32 | selectionmode, 33 | radial_align, 34 | edgeweight, 35 | grabapplymat, 36 | extend, 37 | quickmaterial, 38 | thicken, 39 | panel, 40 | dimensions, 41 | preferences, 42 | quickboolean, 43 | naming, 44 | linear_deformer, 45 | ) 46 | 47 | 48 | class rmKitPannel_parent( bpy.types.Panel ): 49 | bl_idname = "VIEW3D_PT_RMKIT_PARENT" 50 | bl_label = "rmKit" 51 | bl_space_type = "VIEW_3D" 52 | bl_region_type = "UI" 53 | bl_category = "rmKit" 54 | 55 | def draw( self, context ): 56 | layout = self.layout 57 | 58 | 59 | def register(): 60 | bpy.utils.register_class( rmKitPannel_parent ) 61 | propertygroup.register() 62 | polypatch.register() 63 | reduce.register() 64 | context_bevel.register() 65 | loopring.register() 66 | move_to_furthest.register() 67 | knifescreen.register() 68 | extrudealongpath.register() 69 | connect_edges.register() 70 | arcadjust.register() 71 | targetweld.register() 72 | createtube.register() 73 | copypaste.register() 74 | cursor.register() 75 | workplane.register() 76 | linear_deformer.register() 77 | screenreflect.register() 78 | selectionmode.register() 79 | radial_align.register() 80 | edgeweight.register() 81 | grabapplymat.register() 82 | extend.register() 83 | quickmaterial.register() 84 | thicken.register() 85 | panel.register() 86 | vnormals.register() 87 | dimensions.register() 88 | quickboolean.register() 89 | preferences.register() 90 | naming.register() 91 | 92 | 93 | def unregister(): 94 | bpy.utils.unregister_class( rmKitPannel_parent ) 95 | propertygroup.unregister() 96 | polypatch.unregister() 97 | reduce.unregister() 98 | context_bevel.unregister() 99 | loopring.unregister() 100 | move_to_furthest.unregister() 101 | knifescreen.unregister() 102 | extrudealongpath.unregister() 103 | connect_edges.unregister() 104 | arcadjust.unregister() 105 | targetweld.unregister() 106 | createtube.unregister() 107 | copypaste.unregister() 108 | cursor.unregister() 109 | workplane.unregister() 110 | linear_deformer.unregister() 111 | screenreflect.unregister() 112 | selectionmode.unregister() 113 | radial_align.unregister() 114 | edgeweight.unregister() 115 | grabapplymat.unregister() 116 | extend.unregister() 117 | quickmaterial.unregister() 118 | thicken.unregister() 119 | panel.unregister() 120 | vnormals.unregister() 121 | dimensions.unregister() 122 | quickboolean.unregister() 123 | preferences.unregister() 124 | naming.unregister() -------------------------------------------------------------------------------- /arcadjust.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import rmlib 4 | import mathutils 5 | 6 | def get_vec( v_a, v_b, face_normals ): 7 | avg_nml = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 8 | avg_nml_backup = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 9 | e = rmlib.rmEdgeSet.from_endpoints( v_a, v_b ) 10 | for f in v_a.link_faces: 11 | if f not in e.link_faces: 12 | avg_nml += face_normals[f] 13 | else: 14 | avg_nml_backup += face_normals[f] 15 | if avg_nml.length < 0.0001: 16 | avg_nml = avg_nml_backup.normalized() 17 | else: 18 | avg_nml.normalize() 19 | edge_vec = v_b.co - v_a.co 20 | edge_vec.normalize() 21 | cross = avg_nml.cross( edge_vec ) 22 | cross.normalize() 23 | edge_vec = cross.cross( avg_nml ) 24 | edge_vec.normalize() 25 | return edge_vec 26 | 27 | def ScaleLine( p0, p1, scale ): 28 | v = p1 - p0 29 | m = v.length 30 | v.normalize() 31 | p0 -= v * m * 0.5 * scale 32 | p1 += v * m * 0.5 * scale 33 | return ( p0, p1 ) 34 | 35 | def arc_adjust( bm, scale ): 36 | edges = rmlib.rmEdgeSet( [ e for e in bm.edges if e.select ] ) 37 | 38 | normals_cache = {} 39 | for v in edges.vertices: 40 | for f in v.link_faces: 41 | normals_cache[f] = f.normal.copy() 42 | 43 | chains = edges.chain() 44 | for chain in chains: 45 | if len( chain ) < 3: 46 | v_a = get_vec( chain[0][0], chain[0][1], normals_cache ) 47 | v_b = get_vec( chain[-1][-1], chain[-1][-2], normals_cache ) 48 | a, b = ScaleLine( chain[0][0].co.copy(), chain[0][0].co.copy() + v_a, 10000.0 ) 49 | c, d = ScaleLine( chain[-1][-1].co.copy(), chain[-1][-1].co.copy() + v_b , 10000.0 ) 50 | else: 51 | a, b = ScaleLine( chain[0][0].co.copy(), chain[0][1].co.copy(), 10000.0 ) 52 | c, d = ScaleLine( chain[-1][-1].co.copy(), chain[-1][-2].co.copy() , 10000.0 ) 53 | 54 | try: 55 | p0, p1 = mathutils.geometry.intersect_line_line( a, b, c, d ) 56 | except TypeError: 57 | return False 58 | 59 | c = ( p0 + p1 ) * 0.5 60 | s = mathutils.Matrix.Identity( 3 ) 61 | s[0][0] = scale 62 | s[1][1] = scale 63 | s[2][2] = scale 64 | 65 | verts = rmlib.rmVertexSet() 66 | if len( chain ) == 1: 67 | verts = list( chain[0] ) 68 | elif len( chain ) == 2: 69 | verts = list( chain[0] ) 70 | verts.append( chain[1][1] ) 71 | else: 72 | for pair in chain[1:]: 73 | if pair[0] not in verts: 74 | verts.append( pair[0] ) 75 | for v in verts: 76 | pos = v.co - c 77 | pos = s @ pos 78 | v.co = pos + c 79 | 80 | if abs( scale ) <= 0.0000001: 81 | bmesh.ops.remove_doubles( bm, verts=verts, dist=0.00001 ) 82 | 83 | return True 84 | 85 | 86 | def ComputePlane( chain ): 87 | a, b = ScaleLine( chain[0].co.copy(), chain[1].co.copy(), 10000.0 ) 88 | c, d = ScaleLine( chain[-1].co.copy(), chain[-2].co.copy() , 10000.0 ) 89 | result = mathutils.geometry.intersect_line_line( a, b, c, d ) 90 | if result is None: 91 | return None, None 92 | plane_pos = ( result[0] + result[1] ) * 0.5 93 | 94 | start_vec = chain[1].co - plane_pos 95 | start_vec.normalize() 96 | 97 | end_vec = chain[-2].co - plane_pos 98 | end_vec.normalize() 99 | 100 | plane_normal = start_vec.cross( end_vec ) 101 | plane_normal.normalize() 102 | 103 | return plane_pos, plane_normal 104 | 105 | 106 | def ComputeCircleCenter( chain ): 107 | plane_pos, plane_normal = ComputePlane( chain ) 108 | if plane_pos is None: 109 | return None, None 110 | 111 | start_vec = plane_pos - chain[1].co 112 | start_vec.normalize() 113 | 114 | end_vec = plane_pos - chain[-2].co 115 | end_vec.normalize() 116 | 117 | start_vec = start_vec.cross( plane_normal ) 118 | start_vec.normalize() 119 | 120 | end_vec = plane_normal.cross( end_vec ) 121 | end_vec.normalize() 122 | 123 | a, b = ScaleLine( chain[1].co.copy(), chain[1].co.copy() + start_vec, 10000.0 ) 124 | c, d = ScaleLine( chain[-2].co.copy(), chain[-2].co.copy() + end_vec , 10000.0 ) 125 | try: 126 | p0, p1 = mathutils.geometry.intersect_line_line( a, b, c, d ) 127 | except TypeError: 128 | #no intersection 129 | return None, None 130 | circle_center = ( p0 + p1 ) * 0.5 131 | 132 | diagonal_vector = ( circle_center - plane_pos ).normalized() 133 | 134 | return circle_center, diagonal_vector 135 | 136 | 137 | def GetFirstChainIdx( bm, chains ): 138 | active_edge = bm.select_history.active 139 | for i in range( len( chains ) ): 140 | for v in chains[i]: 141 | for e in v.link_edges: 142 | if e == active_edge: 143 | return i 144 | return 0 145 | 146 | 147 | def RemapChain( chain, nearest_center ): 148 | plane_pos, plane_normal = ComputePlane( chain ) 149 | d = rmlib.util.PlaneDistance( nearest_center, chain[1].co.copy(), plane_normal ) 150 | center = nearest_center - ( plane_normal * d ) 151 | 152 | vec = ( chain[0].co.copy() - chain[1].co.copy() ).normalized() 153 | in_vec = vec.cross( plane_normal ) 154 | a, b = ScaleLine( center.copy(), center.copy() + in_vec, 10000.0 ) 155 | c, d = ScaleLine( chain[1].co.copy(), chain[0].co.copy(), 10000.0 ) 156 | foo, start_p = mathutils.geometry.intersect_line_line( a, b, c, d ) 157 | 158 | vec = ( chain[-1].co.copy() - chain[-2].co.copy() ).normalized() 159 | in_vec = vec.cross( plane_normal ) 160 | a, b = ScaleLine( center.copy(), center.copy() + in_vec, 10000.0 ) 161 | c, d = ScaleLine( chain[-2].co.copy(), chain[-1].co.copy(), 10000.0 ) 162 | foo, end_p = mathutils.geometry.intersect_line_line( a, b, c, d ) 163 | 164 | start_vec = ( start_p - center ).normalized() 165 | end_vec = ( end_p - center ).normalized() 166 | 167 | cross_vec = start_vec.cross( end_vec ) 168 | cross_vec.normalize() 169 | if plane_normal.dot( cross_vec ) < 0: 170 | plane_normal *= -1.0 171 | 172 | radian_step = start_vec.angle( end_vec ) / ( len( chain ) - 3 ) 173 | rot_quat = mathutils.Quaternion( plane_normal, radian_step ) 174 | 175 | vec = ( start_p - center ) 176 | radius = vec.length 177 | vec.normalize() 178 | chain[1].co = start_p 179 | for i in range( 2, len( chain ) - 1 ): 180 | vec.rotate( rot_quat ) 181 | chain[i].co = vec * radius + center 182 | 183 | 184 | def radial_arc_adjust( bm, scale ): 185 | edges = rmlib.rmEdgeSet( [ e for e in bm.edges if e.select ] ) 186 | 187 | chains = edges.vert_chain() 188 | 189 | min_edgecount_in_chains = 999999 190 | for chain in chains: 191 | if len( chain ) < min_edgecount_in_chains: 192 | min_edgecount_in_chains = len( chain ) 193 | if min_edgecount_in_chains <= 3: 194 | return arc_adjust( bm, scale ) 195 | 196 | for i in range( 1, len( chains ) ): 197 | a = ( chains[i][-2].co - chains[0][1].co ).length 198 | b = ( chains[i][1].co - chains[0][1].co ).length 199 | if a < b: 200 | chains[i].reverse() 201 | 202 | chain_idx = GetFirstChainIdx( bm, chains ) 203 | circle_center, diagonal_vector = ComputeCircleCenter( chains[chain_idx] ) 204 | if circle_center is None: 205 | return False 206 | circle_center += diagonal_vector * ( scale - 1.0 ) 207 | 208 | for chain in chains: 209 | if len( chain ) < 4: 210 | continue 211 | RemapChain( chain, circle_center ) 212 | 213 | return True 214 | 215 | class MESH_OT_arcadjust( bpy.types.Operator ): 216 | """Interpert continuous selections of edges as circular arcs and scale them.""" 217 | bl_idname = 'mesh.rm_arcadjust' 218 | bl_label = 'Arc Adjust' 219 | bl_options = { 'REGISTER', 'UNDO' } 220 | 221 | scale: bpy.props.FloatProperty( 222 | name='Scale', 223 | description='Scale applied to selected arc', 224 | default=1.0 225 | ) 226 | 227 | radial: bpy.props.BoolProperty( 228 | name='Radial', 229 | default=False 230 | ) 231 | 232 | @classmethod 233 | def poll( cls, context ): 234 | return ( context.area.type == 'VIEW_3D' and 235 | len( context.editable_objects ) > 0 and 236 | context.object is not None and 237 | context.mode == 'EDIT_MESH' and 238 | context.object.type == 'MESH' and 239 | context.tool_settings.mesh_select_mode[:][1] ) 240 | 241 | def cleanup( self ): 242 | if hasattr(self, "bmList"): 243 | for bm in self.bmList: 244 | bm.free() 245 | self.bmList.clear() 246 | if hasattr(self, "meshList"): 247 | self.meshList.clear() 248 | 249 | def cancel( self, context ): 250 | self.cleanup() 251 | 252 | def execute( self, context ): 253 | bpy.ops.object.mode_set( mode='OBJECT', toggle=False ) 254 | 255 | for i, bmlistelem in enumerate( self.bmList ): 256 | bm = bmlistelem.copy() 257 | 258 | if self.radial: 259 | success = radial_arc_adjust( bm, self.scale - 1.0 ) 260 | if not success: 261 | self.report( { 'WARNING' }, 'Radial Arc Adjust failed!!!' ) 262 | bpy.ops.object.mode_set( mode='EDIT', toggle=False ) 263 | bm.free() 264 | continue 265 | else: 266 | result = arc_adjust( bm, self.scale ) 267 | if not result: 268 | self.report( { 'WARNING' }, 'Arc Adjust failed!!!' ) 269 | bpy.ops.object.mode_set( mode='EDIT', toggle=False ) 270 | bm.free() 271 | continue 272 | 273 | targetMesh = self.meshList[i] 274 | bm.to_mesh( targetMesh ) 275 | bm.calc_loop_triangles() 276 | targetMesh.update() 277 | bm.free() 278 | 279 | bpy.ops.object.mode_set( mode='EDIT', toggle=False ) 280 | 281 | return { 'FINISHED' } 282 | 283 | def modal( self, context, event ): 284 | if event.type == 'LEFTMOUSE': 285 | return { 'FINISHED' } 286 | elif event.type == 'RIGHTMOUSE': 287 | if event.value == 'RELEASE': 288 | self.radial = not self.radial 289 | elif event.type == 'MOUSEMOVE': 290 | delta_x = float( event.mouse_x - event.mouse_prev_press_x ) / context.region.width 291 | #delta_y = float( event.mouse_prev_press_y - event.mouse_y ) / context.region.height 292 | self.scale = 1.0 + ( delta_x * 4.0 ) 293 | self.execute( context ) 294 | elif event.type == 'ESC': 295 | return { 'CANCELLED' } 296 | 297 | return { 'RUNNING_MODAL' } 298 | 299 | def invoke( self, context, event ): 300 | self.meshList = [] 301 | self.bmList = [] 302 | 303 | includes_invalid_selection = False 304 | 305 | for rmmesh in rmlib.iter_edit_meshes( context ): 306 | with rmmesh as rmmesh: 307 | 308 | edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 309 | chains = edges.chain() 310 | for chain in chains: 311 | if len( chain ) <= 2: 312 | includes_invalid_selection = True 313 | break 314 | if chain[0][0] == chain[-1][-1]: 315 | includes_invalid_selection = True 316 | break 317 | 318 | 319 | rmmesh.readme = True 320 | self.meshList.append( rmmesh.mesh ) 321 | self.bmList.append( rmmesh.bmesh.copy() ) 322 | 323 | if includes_invalid_selection: 324 | self.report( { 'WARNING' }, 'Includes invalid edge selection. Selected loops must not be closed ang be longer than 2 edges each.' ) 325 | return { 'CANCELLED' } 326 | 327 | context.window_manager.modal_handler_add( self ) 328 | return { 'RUNNING_MODAL' } 329 | 330 | 331 | class MESH_OT_unbevel( bpy.types.Operator ): 332 | """Interpret continuouse selections of edges as circular arcs and collapse them to an arc of radius 0.0.""" 333 | bl_idname = 'mesh.rm_unbevel' 334 | bl_label = 'Unbevel' 335 | bl_options = { 'UNDO' } 336 | 337 | @classmethod 338 | def poll( cls, context ): 339 | return ( context.area.type == 'VIEW_3D' and len( context.editable_objects ) > 0 ) 340 | 341 | def execute( self, context ): 342 | #get the selection mode 343 | if context.object is None or context.mode == 'OBJECT': 344 | return { 'CANCELLED' } 345 | 346 | if context.object.type != 'MESH': 347 | return { 'CANCELLED' } 348 | 349 | sel_mode = context.tool_settings.mesh_select_mode[:] 350 | if not sel_mode[1]: 351 | return { 'CANCELLED' } 352 | 353 | for rmmesh in rmlib.iter_edit_meshes( context ): 354 | with rmmesh as rmmesh: 355 | result = arc_adjust( rmmesh.bmesh, 0.0 ) 356 | if not result: 357 | self.report( { 'WARNING' }, 'Invalid edge selection!!!' ) 358 | return { 'CANCELLED' } 359 | 360 | return { 'FINISHED' } 361 | 362 | 363 | def register(): 364 | bpy.utils.register_class( MESH_OT_arcadjust ) 365 | bpy.utils.register_class( MESH_OT_unbevel ) 366 | 367 | def unregister(): 368 | bpy.utils.unregister_class( MESH_OT_arcadjust ) 369 | bpy.utils.unregister_class( MESH_OT_unbevel ) -------------------------------------------------------------------------------- /blender_manifest.toml: -------------------------------------------------------------------------------- 1 | schema_version = "1.0.0" 2 | id = "rmKit" 3 | version = "1.0.11" 4 | name = "rmKit" 5 | tagline = "Tools and Utils for Modeling and GameWorkflows" 6 | maintainer = "Timothee Yeramian " 7 | type = "add-on" 8 | 9 | license = [ 10 | "SPDX:GPL-3.0-or-later", 11 | ] 12 | 13 | website = "https://rmkit.readthedocs.io/en/latest/" 14 | tags = ["3D View", "Modeling", "Mesh", "Object", "Material"] 15 | 16 | blender_version_min = "4.2.0" 17 | 18 | wheels = [ 19 | "./wheels/rmlib-0.1.2-py3-none-any.whl" 20 | ] 21 | 22 | [build] 23 | paths_exclude_pattern = [ 24 | "__pycache__/", 25 | ".git", 26 | ".*", 27 | "*.zip", 28 | "*.bat", 29 | "*.hot", 30 | "*.md", 31 | ] -------------------------------------------------------------------------------- /build_extension.bat: -------------------------------------------------------------------------------- 1 | blender --command extension build 2 | @echo off 3 | echo Press any key to exit . . . 4 | pause>nul -------------------------------------------------------------------------------- /context_bevel.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import rmlib 3 | 4 | class MESH_OT_contextbevel( bpy.types.Operator ): 5 | """Activate appropriate bevel tool based on selection mode.""" 6 | bl_idname = 'mesh.rm_contextbevel' 7 | bl_label = 'Context Bevel' 8 | 9 | @classmethod 10 | def poll( cls, context ): 11 | return ( context.area.type == 'VIEW_3D' and 12 | context.active_object is not None and 13 | context.active_object.type == 'MESH' and 14 | context.object.data.is_editmode ) 15 | 16 | def execute( self, context ): 17 | sel_mode = context.tool_settings.mesh_select_mode[:] 18 | if sel_mode[0]: #vert mode 19 | bpy.ops.mesh.bevel( 'INVOKE_DEFAULT', affect='VERTICES' ) 20 | elif sel_mode[1]: #edge mode 21 | rmmesh = rmlib.rmMesh.GetActive( context ) 22 | with rmmesh as rmmesh: 23 | rmmesh.readonly = True 24 | sel_edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 25 | if len( sel_edges ) > 0: 26 | bpy.ops.mesh.bevel( 'INVOKE_DEFAULT', affect='EDGES' ) 27 | else: 28 | return { 'CANCELLED' } 29 | if sel_mode[2]: #poly mode 30 | bpy.ops.mesh.inset( 'INVOKE_DEFAULT', use_outset=False ) 31 | 32 | return { 'FINISHED' } 33 | 34 | def register(): 35 | bpy.utils.register_class( MESH_OT_contextbevel ) 36 | 37 | def unregister(): 38 | bpy.utils.unregister_class( MESH_OT_contextbevel ) -------------------------------------------------------------------------------- /copypaste.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bpy_extras 3 | import rmlib 4 | import math 5 | import mathutils 6 | import bmesh 7 | 8 | def copy( context, cut=False ): 9 | rmmesh = rmlib.rmMesh.GetActive( context ) 10 | if rmmesh is None: 11 | return 12 | 13 | with rmmesh as rmmesh: 14 | rmmesh.readonly = not cut 15 | 16 | clipboard_mesh = None 17 | for m in bpy.data.meshes: 18 | if m.name == 'clipboard_mesh': 19 | clipboard_mesh = m 20 | break 21 | if clipboard_mesh is None: 22 | clipboard_mesh = bpy.data.meshes.new( 'clipboard_mesh' ) 23 | clipboard_mesh.clear_geometry() 24 | clipboard_mesh.materials.clear() 25 | 26 | temp_bmesh = bmesh.new() 27 | temp_bmesh = rmmesh.bmesh.copy() 28 | 29 | selected_polys = rmlib.rmPolygonSet.from_selection( rmmesh ) 30 | selected_polygon_indexes = [ p.index for p in selected_polys ] 31 | 32 | mat_indexes = [] 33 | delPolys = rmlib.rmPolygonSet() 34 | for i, p in enumerate( temp_bmesh.faces ): 35 | if i in selected_polygon_indexes: 36 | if p.material_index not in mat_indexes: 37 | mat_indexes.append( p.material_index ) 38 | p.material_index = mat_indexes.index( p.material_index ) 39 | else: 40 | delPolys.append( p ) 41 | bmesh.ops.delete( temp_bmesh, geom=delPolys, context='FACES' ) 42 | 43 | #bring clipboard_mesh into world space 44 | xfrm = rmmesh.world_transform 45 | for v in temp_bmesh.verts: 46 | v.co = xfrm @ v.co.copy() 47 | 48 | temp_bmesh.to_mesh( clipboard_mesh ) 49 | 50 | if len( rmmesh.mesh.materials ) > 0: 51 | for old_idx in mat_indexes: 52 | clipboard_mesh.materials.append( rmmesh.mesh.materials[old_idx] ) 53 | 54 | if cut: 55 | bmesh.ops.delete( rmmesh.bmesh, geom=selected_polys, context='FACES' ) 56 | 57 | 58 | def paste( context ): 59 | clipboard_mesh = None 60 | for m in bpy.data.meshes: 61 | if m.name == 'clipboard_mesh': 62 | clipboard_mesh = m 63 | break 64 | if clipboard_mesh is None: 65 | return { 'CANCELLED' } 66 | 67 | rmmesh = rmlib.rmMesh.GetActive( context ) 68 | if rmmesh is None: 69 | return 70 | with rmmesh as rmmesh: 71 | rmmesh.bmesh.verts.ensure_lookup_table() 72 | for p in rmmesh.bmesh.faces: 73 | p.select = False 74 | 75 | rmmesh.bmesh.from_mesh( clipboard_mesh ) 76 | rmmesh.bmesh.verts.ensure_lookup_table() 77 | 78 | #map material indexes from clipboard_mesh to rmmesh and add new mats to rmmesh 79 | mat_lookup = [] 80 | for clip_mat in clipboard_mesh.materials: 81 | matFound = False 82 | for i, src_mat in enumerate( rmmesh.mesh.materials ): 83 | if clip_mat == src_mat: 84 | mat_lookup.append( i ) 85 | matFound = True 86 | break 87 | if not matFound: 88 | mat_lookup.append( len( rmmesh.mesh.materials ) ) 89 | rmmesh.mesh.materials.append( clip_mat ) 90 | 91 | paste_faces = rmlib.rmPolygonSet.from_selection( rmmesh ) 92 | 93 | #reassign material indexes 94 | if len( mat_lookup ) > 0: 95 | for f in paste_faces: 96 | f.material_index = mat_lookup[ f.material_index ] 97 | 98 | #transform paste verts 99 | xfrm_inv = rmmesh.world_transform.inverted() 100 | for v in paste_faces.vertices: 101 | v.co = xfrm_inv @ mathutils.Vector( v.co ) 102 | 103 | 104 | class MESH_OT_rm_copy( bpy.types.Operator ): 105 | """Runs copybuffer in object mode. In polygon mode is copies/cuts the polygon selection to the clipboard.""" 106 | bl_idname = 'mesh.rm_copy' 107 | bl_label = 'Copy' 108 | 109 | cut: bpy.props.BoolProperty( 110 | name='Cut', 111 | default=False 112 | ) 113 | 114 | @classmethod 115 | def poll( cls, context ): 116 | return ( context.area.type == 'VIEW_3D' and context.object is not None ) 117 | 118 | def execute( self, context ): 119 | if context.object.type == 'MESH' and context.object.data.is_editmode: 120 | sel_mode = context.tool_settings.mesh_select_mode[:] 121 | if sel_mode[1]: 122 | bpy.ops.mesh.rm_connectedge('INVOKE_DEFAULT') 123 | elif sel_mode[2]: 124 | copy( context, self.cut ) 125 | else: 126 | bpy.ops.view3d.copybuffer() 127 | 128 | return { 'FINISHED' } 129 | 130 | 131 | class MESH_OT_rm_paste( bpy.types.Operator ): 132 | """Runs pastebuffer in object mode. In polygon mode is pastes the polygon selection into the current mesh.""" 133 | bl_idname = 'mesh.rm_paste' 134 | bl_label = 'Paste' 135 | bl_options = { 'UNDO' } 136 | 137 | @classmethod 138 | def poll( cls, context ): 139 | return ( context.area.type == 'VIEW_3D' and context.object is not None ) 140 | 141 | def execute( self, context ): 142 | if context.object.type == 'MESH' and context.object.data.is_editmode: 143 | sel_mode = context.tool_settings.mesh_select_mode[:] 144 | if sel_mode[2]: 145 | paste( context ) 146 | else: 147 | bpy.ops.view3d.pastebuffer() 148 | return { 'FINISHED' } 149 | 150 | 151 | def register(): 152 | bpy.utils.register_class( MESH_OT_rm_copy ) 153 | bpy.utils.register_class( MESH_OT_rm_paste ) 154 | 155 | 156 | def unregister(): 157 | bpy.utils.unregister_class( MESH_OT_rm_copy ) 158 | bpy.utils.unregister_class( MESH_OT_rm_paste ) 159 | 160 | 161 | if __name__ == '__main__': 162 | register() -------------------------------------------------------------------------------- /createtube.py: -------------------------------------------------------------------------------- 1 | from operator import inv 2 | from re import T 3 | import bpy 4 | import bmesh 5 | import rmlib 6 | import mathutils 7 | import math 8 | 9 | class Tube(): 10 | index = -1 11 | 12 | def __init__( self ): 13 | self.centers = [] 14 | self.normals = [] 15 | self.vec = mathutils.Vector( ( 0.0, 0.0, 1.0 ) ) 16 | self.closed = False 17 | self.poly = None 18 | self.__length = -1.0 19 | 20 | self.idx = Tube.index 21 | Tube.index += 1 22 | 23 | @property 24 | def length( self ): 25 | if self.__length < 0.0: 26 | self.__length = 0.0 27 | for i in range( int( not self.closed ), len( self.centers ) ): 28 | self.__length += ( self.centers[i-1] - self.centers[i] ).length 29 | 30 | return self.__length 31 | 32 | 33 | class MESH_OT_createtube( bpy.types.Operator ): 34 | """Creates a generalized cylinder abound the edge selection. If a generalized cylinder is selected in polygon mode, then a new ones is recreated.""" 35 | bl_idname = 'mesh.rm_createtube' 36 | bl_label = 'Create Tube' 37 | bl_options = { 'REGISTER', 'UNDO' } 38 | 39 | level: bpy.props.IntProperty( 40 | name='Level', 41 | default=8, 42 | min=3 43 | ) 44 | radius: bpy.props.FloatProperty( 45 | name='Radius', 46 | default=0.1 47 | ) 48 | 49 | degrees: bpy.props.FloatProperty( 50 | name='Angle', 51 | default=0.0, 52 | min=-180.0, 53 | max=180.0 54 | ) 55 | 56 | def cancel( self, context ): 57 | if hasattr( self, 'bmesh' ): 58 | if self.bmesh is not None: 59 | self.bmesh.free() 60 | self.bmesh = None 61 | 62 | if hasattr( self, '_tubes' ): 63 | self._tubes.clear() 64 | 65 | @classmethod 66 | def poll( cls, context ): 67 | return ( context.area.type == 'VIEW_3D' and 68 | context.object is not None and 69 | context.active_object is not None and 70 | context.active_object.type == 'MESH' and 71 | context.object.data.is_editmode and 72 | not context.tool_settings.mesh_select_mode[:][0] ) 73 | 74 | def LocalizeNewBMesh( self ): 75 | bm = self.bmesh.copy() 76 | bm.verts.ensure_lookup_table() 77 | bm.edges.ensure_lookup_table() 78 | bm.faces.ensure_lookup_table() 79 | for v in bm.verts: 80 | v.tag = False 81 | for p in bm.faces: 82 | p.tag = False 83 | return bm 84 | 85 | def execute( self, context ): 86 | bpy.ops.object.mode_set( mode='OBJECT', toggle=False ) 87 | 88 | #localize writable mesh 89 | bm = self.LocalizeNewBMesh() 90 | 91 | #get/create uv layer 92 | uv_layer = bm.loops.layers.uv.verify() 93 | 94 | #create tubes 95 | for tube in self._tubes: 96 | use_tube_normals = len( tube.normals ) == len( tube.centers ) 97 | 98 | #create new vertices 99 | rings = [] 100 | nml = tube.vec 101 | for i in range( len( tube.centers ) ): 102 | currPos = tube.centers[i] 103 | if tube.closed or ( i != 0 and i != len( tube.centers ) - 1 ): 104 | prevPos = tube.centers[i-1] 105 | try: 106 | nextPos = tube.centers[i+1] 107 | except IndexError: 108 | nextPos = tube.centers[0] 109 | A = ( currPos - prevPos ).normalized() 110 | B = ( nextPos - currPos ).normalized() 111 | tan = ( A + B ).normalized() 112 | else: 113 | if i == 0: 114 | nextPos = tube.centers[i+1] 115 | tan = ( nextPos - currPos ).normalized() 116 | else: 117 | prevPos = tube.centers[i-1] 118 | tan = ( currPos - prevPos ).normalized() 119 | if use_tube_normals: 120 | bitan = tan.cross( tube.normals[i] ).normalized() 121 | else: 122 | bitan = tan.cross( nml ).normalized() 123 | nml = bitan.cross( tan ).normalized() 124 | 125 | verts = [] 126 | offset_quat = mathutils.Quaternion( tan, math.radians( self.degrees ) ) 127 | rot_quat = mathutils.Quaternion( tan, math.pi * 2.0 / self.level ) 128 | bitan.rotate( offset_quat ) 129 | for j in range( self.level ): 130 | new_vert = bm.verts.new( currPos + ( bitan * self.radius ) ) 131 | verts.append( new_vert ) 132 | bitan.rotate( rot_quat ) 133 | rings.append( verts ) 134 | 135 | #create faces and set uv data 136 | current_length = 0.0 137 | u_step = 1.0 / float( self.level ) 138 | for i in range( int( not tube.closed ), len( rings ) ): 139 | next_length = current_length + ( tube.centers[i] - tube.centers[i-1] ).length 140 | for j in range( self.level ): 141 | verts = ( rings[i-1][j-1], rings[i-1][j], rings[i][j], rings[i][j-1] ) 142 | face = bm.faces.new( verts ) 143 | face.loops[0][uv_layer].uv = ( u_step * j, current_length / tube.length ) 144 | face.loops[1][uv_layer].uv = ( u_step * ( j + 1 ), current_length / tube.length ) 145 | face.loops[2][uv_layer].uv = ( u_step * ( j + 1 ), next_length / tube.length ) 146 | face.loops[3][uv_layer].uv = ( u_step * j, next_length / tube.length ) 147 | current_length = next_length 148 | 149 | targetMesh = context.active_object.data 150 | bm.to_mesh( targetMesh ) 151 | bm.calc_loop_triangles() 152 | targetMesh.update() 153 | bm.free() 154 | 155 | bpy.ops.object.mode_set( mode='EDIT', toggle=False ) 156 | 157 | return { 'FINISHED' } 158 | 159 | def modal( self, context, event ): 160 | if event.type == 'LEFTMOUSE': 161 | return { 'FINISHED' } 162 | elif event.type == 'MOUSEMOVE': 163 | mouse_current_2d = mathutils.Vector( ( event.mouse_region_x, event.mouse_region_y ) ) 164 | rmvp = rmlib.rmViewport( context ) 165 | mouse_pos_current = rmvp.get_mouse_on_plane( context, mathutils.Vector(), self.dir_vec, mouse_current_2d ) 166 | try: 167 | vOffset = rmlib.util.ProjectVector( mouse_pos_current - self.mouse_pos_prev, self.mouse_pos_prev - self.mouse_pos_start ) 168 | except ZeroDivisionError: 169 | vOffset = mathutils.Vector() 170 | fSign = 1.0 171 | if mouse_current_2d.x < self.mouse_prev_2d.x: 172 | fSign = -1.0 173 | fDelta = vOffset.length * fSign 174 | if event.shift: 175 | fDelta *= 0.05 176 | self.mouse_pos_prev = mouse_pos_current 177 | self.mouse_prev_2d = mouse_current_2d 178 | self.radius += fDelta 179 | self.radius = max( 0.001, self.radius ) 180 | self.execute( context ) 181 | elif event.type == 'WHEELUPMOUSE': 182 | self.level = min( self.level + 1, 128 ) 183 | elif event.type == 'WHEELDOWNMOUSE': 184 | self.level = max( self.level - 1, 3 ) 185 | elif event.type == 'ESC': 186 | return { 'CANCELLED' } 187 | 188 | return { 'RUNNING_MODAL' } 189 | 190 | def invoke( self, context, event ): 191 | self.bmesh = None 192 | self._tubes = [] 193 | 194 | self.radius = 0.001 195 | sel_mode = context.tool_settings.mesh_select_mode[:] 196 | 197 | self.mouse_prev_2d = mathutils.Vector( ( event.mouse_region_x, event.mouse_region_y ) ) 198 | rmvp = rmlib.rmViewport( context ) 199 | row_idx, self.dir_vec, foovec = rmvp.get_nearest_direction_vector( 'front' ) 200 | 201 | rmmesh = rmlib.rmMesh.GetActive( context ) 202 | if rmmesh is not None: 203 | with rmmesh as rmmesh: 204 | rmmesh.readme = True 205 | 206 | for v in rmmesh.bmesh.verts: 207 | v.tag = False 208 | for p in rmmesh.bmesh.faces: 209 | p.tag = False 210 | 211 | self.bmesh = rmmesh.bmesh.copy() 212 | 213 | vAvg = mathutils.Vector() 214 | nSelCount = 0 215 | for v in rmmesh.bmesh.verts: 216 | if v.select: 217 | nSelCount += 1 218 | vAvg += v.co 219 | vAvg *= 1.0 / nSelCount 220 | vAvg = rmmesh.world_transform @ vAvg 221 | 222 | self.mouse_pos_prev = rmvp.get_mouse_on_plane( context, vAvg, self.dir_vec, self.mouse_prev_2d ) 223 | self.mouse_pos_start = self.mouse_pos_prev.copy() 224 | 225 | #cache Tube objects for edge selection 226 | if sel_mode[1]: 227 | edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 228 | 229 | chains = edges.chain() 230 | for i, chain in enumerate( chains ): 231 | tube = Tube() 232 | 233 | tube.closed = chain[0][0] == chain[-1][-1] 234 | 235 | for pair in chain: 236 | tube.centers.append( pair[0].co.copy() ) 237 | tube.normals.append( pair[0].normal.copy() ) 238 | if not tube.closed: 239 | tube.centers.append( chain[-1][-1].co.copy() ) 240 | tube.normals.append( chain[-1][-1].normal.copy() ) 241 | 242 | first_edge = rmlib.rmEdgeSet.from_endpoints( chain[0][0], chain[0][1] ) 243 | try: 244 | tube.poly = first_edge.link_faces[0].index 245 | except IndexError: 246 | pass 247 | 248 | self._tubes.append( tube ) 249 | 250 | #cache Tube objs from face selection 251 | elif sel_mode[2]: 252 | faces = rmlib.rmPolygonSet.from_selection( rmmesh ) 253 | 254 | #init bbox_dist for haul sensitivity 255 | verts = faces.vertices 256 | min = verts[0].co.copy() 257 | max = verts[0].co.copy() 258 | for v in verts: 259 | for i in range( 3 ): 260 | if v.co[i] < min[i]: 261 | min[i] = v.co[i] 262 | if v.co[i] > max[i]: 263 | max[i] = v.co[i] 264 | self.bbox_dist = ( max - min ).length 265 | 266 | groups = faces.group() 267 | for group in groups: 268 | #ensure group is all quads 269 | allQuads = True 270 | for p in group: 271 | if len( p.verts ) != 4: 272 | allQuads = False 273 | break 274 | if not allQuads: 275 | continue 276 | 277 | #ensure exactly two continuous closed loops of open edges (no caps) 278 | chains = rmlib.rmEdgeSet( [ e for e in group.edges if e.is_boundary ] ).chain() 279 | if len( chains ) != 2: 280 | continue 281 | if len( chains[0] ) < 3: 282 | continue 283 | if chains[0][0][0] != chains[0][-1][-1] or chains[1][0][0] != chains[1][-1][-1]: 284 | continue 285 | 286 | #ensure each vert either boardered 4 manifold edges or 1 manifold and 2 boundary edges 287 | invalidTopo = False 288 | for v in group.vertices: 289 | boundary_count = 0 290 | contiguous_count = 0 291 | for e in v.link_edges: 292 | if e.is_boundary: 293 | boundary_count += 1 294 | if e.is_contiguous: 295 | contiguous_count += 1 296 | if not ( boundary_count == 2 and contiguous_count == 1 ): 297 | invalidTopo = True 298 | break 299 | if not ( boundary_count == 4 and contiguous_count == 0 ): 300 | invalidTopo = True 301 | break 302 | if not invalidTopo: 303 | continue 304 | 305 | #break up group into list of verts each of same size (rings of tube) 306 | rings = [ rmlib.rmVertexSet( [ pair[0] for pair in chains[0] ] ) ] 307 | for v in rings[-1]: 308 | v.tag = True 309 | while( True ): 310 | new_ring = set() 311 | for p in rings[-1].polygons: 312 | if p.tag: 313 | continue 314 | p.tag = True 315 | for v in p.verts: 316 | if v.tag: 317 | continue 318 | v.tag = True 319 | new_ring.add( v ) 320 | if len( new_ring ) < 3: 321 | break 322 | rings.append( rmlib.rmVertexSet( new_ring ) ) 323 | if len( rings ) < 2: 324 | continue 325 | 326 | #cache Tube obj 327 | tube = Tube() 328 | for ring in rings: 329 | avg = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 330 | for v in ring: 331 | avg += v.co 332 | avg *= 1.0 / len( ring ) 333 | tube.centers.append( avg ) 334 | 335 | tube.vec = groups[0][0].normal.copy() 336 | tube.poly = groups[0][0].index 337 | 338 | self._tubes.append( tube ) 339 | 340 | if len( self._tubes ) < 1: 341 | if sel_mode[2]: 342 | self.report( { 'ERROR' }, 'Did not meet requirements for rebuilding selected tube(s). Topology must be all quads and no caps!!!' ) 343 | return { 'CANCELLED' } 344 | 345 | context.window_manager.modal_handler_add( self ) 346 | return { 'RUNNING_MODAL' } 347 | 348 | 349 | def register(): 350 | bpy.utils.register_class( MESH_OT_createtube ) 351 | 352 | def unregister(): 353 | bpy.utils.unregister_class( MESH_OT_createtube ) -------------------------------------------------------------------------------- /cursor.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh, mathutils 2 | import rmlib 3 | 4 | BACKGROUND_LAYERNAME = 'rm_background' 5 | 6 | def GetSelsetEdges( bm, layername ): 7 | intlayers = bm.edges.layers.int 8 | selset = intlayers.get( layername, None ) 9 | if selset is None: 10 | return rmlib.rmEdgeSet() 11 | return rmlib.rmEdgeSet( [ e for e in bm.edges if bool( e[selset] ) ] ) 12 | 13 | def VecFromEdge( v, e ): 14 | pos1 = mathutils.Vector( v.co.copy() ) 15 | v2 = e.other_vert( v ) 16 | pos2 = mathutils.Vector( v2.co.copy() ) 17 | return ( pos2 - pos1 ).normalized() 18 | 19 | class MESH_OT_cursortoselection( bpy.types.Operator ): 20 | """Move and orient the 3D Cursor to the vert/edge/face selection.""" 21 | bl_idname = 'view3d.rm_cursor_to_selection' 22 | bl_label = 'Move and Orient 3D Cursor to Selection' 23 | bl_options = { 'UNDO' } 24 | 25 | @classmethod 26 | def poll( cls, context ): 27 | return ( context.area.type == 'VIEW_3D' and 28 | context.active_object is not None and 29 | context.active_object.type == 'MESH' ) 30 | 31 | def execute( self, context ): 32 | if context.mode == 'EDIT_MESH': 33 | sel_mode = context.tool_settings.mesh_select_mode[:] 34 | rmmesh = rmlib.rmMesh.GetActive( context ) 35 | if rmmesh is None: 36 | return { 'CANCELLED' } 37 | with rmmesh as rmmesh: 38 | rmmesh.readonly = True 39 | if sel_mode[0]: 40 | sel_verts = rmlib.rmVertexSet.from_selection( rmmesh ) 41 | if len( sel_verts ) > 0: 42 | v = sel_verts[0] 43 | link_edges = rmlib.rmEdgeSet( list( v.link_edges ) ) 44 | 45 | selected_edges = GetSelsetEdges( rmmesh.bmesh, BACKGROUND_LAYERNAME ) 46 | first_edge = None 47 | if len( selected_edges ) > 0: 48 | first_edge = selected_edges[0] 49 | for i, e in enumerate( link_edges ): 50 | if e in selected_edges: 51 | first_edge = link_edges.pop( i ) 52 | break 53 | if first_edge is None: 54 | if len( link_edges ) > 1: 55 | first_edge = link_edges[1] 56 | 57 | if first_edge is None or len( link_edges ) < 1: 58 | v_n = v.normal 59 | v_t = mathutils.Vector( ( 0.0, 0.0001, 1.0 ) ) 60 | for e in v.link_edges: 61 | v1, v2 = e.verts 62 | v_t = v2.co - v1.co 63 | v_t = v_n.cross( v_t.normalized() ) 64 | else: 65 | v_t = VecFromEdge( v, first_edge ) 66 | v_n = VecFromEdge( v, link_edges[0] ) 67 | crossvec = v_t.cross( v_n ).normalized() 68 | v_n = crossvec.cross( v_t ).normalized() 69 | 70 | v_p = rmmesh.world_transform @ v.co.copy() 71 | v_t = rmmesh.world_transform.to_3x3() @ v_t 72 | v_n = rmmesh.world_transform.to_3x3() @ v_n 73 | m4 = rmlib.util.LookAt( v_n, v_t, v_p ) 74 | context.scene.cursor.matrix = m4 75 | context.scene.cursor.location = v_p 76 | 77 | elif sel_mode[1]: 78 | sel_edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 79 | if len( sel_edges ) > 0: 80 | e = sel_edges[0] 81 | 82 | e_n = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 83 | for p in e.link_faces: 84 | e_n += p.normal 85 | if e_n.length < 0.00000001: 86 | mathutils.Vector( ( 0.0, 0.0001, 1.0 ) ) 87 | e_n.normalize() 88 | 89 | v1, v2 = e.verts 90 | e_t = v2.co - v1.co 91 | e_t = e_n.cross( e_t.normalized() ) 92 | 93 | e_p = ( v1.co + v2.co ) * 0.5 94 | 95 | e_p = rmmesh.world_transform @ e_p 96 | e_t = rmmesh.world_transform.to_3x3() @ e_t 97 | e_n = rmmesh.world_transform.to_3x3() @ e_n 98 | m4 = rmlib.util.LookAt( e_n, e_t, e_p ) 99 | context.scene.cursor.matrix = m4 100 | context.scene.cursor.location = e_p 101 | 102 | elif sel_mode[2]: 103 | sel_polys = rmlib.rmPolygonSet.from_selection( rmmesh ) 104 | if len( sel_polys ) > 0: 105 | p = sel_polys[0] 106 | 107 | p_p = rmmesh.world_transform @ p.calc_center_median() 108 | p_t = rmmesh.world_transform.to_3x3() @ p.calc_tangent_edge_pair() 109 | p_n = rmmesh.world_transform.to_3x3() @ p.normal 110 | m4 = rmlib.util.LookAt( p_n, p_t, p_p ) 111 | context.scene.cursor.matrix = m4 112 | context.scene.cursor.location = p_p 113 | 114 | elif context.object is not None and context.mode == 'OBJECT': 115 | obj = context.object 116 | context.scene.cursor.matrix = obj.matrix_world 117 | 118 | #needed to refresh cursor viewport draw 119 | obj.select_set( False ) 120 | obj.select_set( True ) 121 | 122 | return { 'FINISHED' } 123 | 124 | 125 | class MESH_OT_unrotatefromcursor( bpy.types.Operator ): 126 | """Unrotate selection baed on cursor orientation.""" 127 | bl_idname = 'view3d.rm_unrotate_relative_to_cursor' 128 | bl_label = 'Unrotate Relative to Cursor' 129 | bl_options = { 'UNDO' } 130 | 131 | @classmethod 132 | def poll( cls, context ): 133 | return ( context.area.type == 'VIEW_3D' and 134 | context.active_object is not None and 135 | context.active_object.type == 'MESH' ) 136 | 137 | def execute( self, context ): 138 | cursor_pos = mathutils.Vector( context.scene.cursor.location ) 139 | cursor_xfrm = mathutils.Matrix( context.scene.cursor.matrix ) 140 | cursor_xfrm_inv = cursor_xfrm.inverted() 141 | 142 | if context.mode == 'EDIT_MESH': 143 | sel_mode = context.tool_settings.mesh_select_mode[:] 144 | rmmesh = rmlib.rmMesh.GetActive( context ) 145 | if rmmesh is None: 146 | return { 'CANCELLED' } 147 | with rmmesh as rmmesh: 148 | xfrm_inv = rmmesh.world_transform.inverted() 149 | 150 | if sel_mode[0]: 151 | sel_verts = rmlib.rmVertexSet.from_selection( rmmesh ) 152 | for group in sel_verts.group( True ): 153 | for v in group: 154 | v_wld = rmmesh.world_transform @ v.co #bring to world space 155 | v_wld = cursor_xfrm_inv @ v_wld #transform by inverse of cursor 156 | v_wld += cursor_pos 157 | v_obj = xfrm_inv @ v_wld #bring back into obj space 158 | v.co = v_obj 159 | 160 | elif sel_mode[1]: 161 | sel_edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 162 | for egroup in sel_edges.group( True ): 163 | group = egroup.vertices 164 | for v in group: 165 | v_wld = rmmesh.world_transform @ v.co #bring to world space 166 | v_wld = cursor_xfrm_inv @ v_wld #transform by inverse of cursor 167 | v_wld += cursor_pos 168 | v_obj = xfrm_inv @ v_wld #bring back into obj space 169 | v.co = v_obj 170 | 171 | elif sel_mode[2]: 172 | sel_faces = rmlib.rmPolygonSet.from_selection( rmmesh ) 173 | for fgroup in sel_faces.group( True ): 174 | group = fgroup.vertices 175 | for v in group: 176 | v_wld = rmmesh.world_transform @ v.co #bring to world space 177 | v_wld = cursor_xfrm_inv @ v_wld #transform by inverse of cursor 178 | v_wld += cursor_pos 179 | v_obj = xfrm_inv @ v_wld #bring back into obj space 180 | v.co = v_obj 181 | 182 | 183 | elif context.object is not None and context.mode == 'OBJECT': 184 | obj = context.object 185 | obj.matrix_world = cursor_xfrm_inv @ obj.matrix_world 186 | obj.location += cursor_pos 187 | 188 | return { 'FINISHED' } 189 | 190 | 191 | class OBJECT_OT_origintocursor( bpy.types.Operator ): 192 | """Move the pivot point of selected objects to the 3D Cursor. All linked meshes xforms are compensated for this transformation.""" 193 | bl_idname = 'object.rm_origin_to_cursor' 194 | bl_label = 'Pivot to Cursor' 195 | bl_options = { 'UNDO' } 196 | 197 | @classmethod 198 | def poll( cls, context ): 199 | return ( context.area.type == 'VIEW_3D' and 200 | context.active_object is not None and 201 | context.active_object.type == 'MESH' ) 202 | 203 | def execute( self, context ): 204 | ao = context.active_object 205 | if ao.type != 'MESH': 206 | self.report( { 'ERROR' }, 'Active Object must be a mesh.' ) 207 | return 208 | 209 | cursor_pos = context.scene.cursor.location.copy() 210 | cursor_pos_obj = ao.matrix_world.inverted() @ cursor_pos 211 | 212 | most_parent_instance = ao 213 | p = ao.parent 214 | while( p is not None ): 215 | if p.type != 'MESH': 216 | p = p.parent 217 | continue 218 | if p.data == ao.data: 219 | most_parent_instance = p 220 | p = p.parent 221 | ao = most_parent_instance 222 | 223 | instances = [] 224 | for o in context.scene.objects: 225 | if o.type != 'MESH' or o == ao: 226 | continue 227 | if o.data == ao.data: 228 | instances.append( o ) 229 | 230 | prev_mode = context.mode 231 | if prev_mode == 'EDIT_MESH': 232 | prev_mode = 'EDIT' 233 | bpy.ops.object.mode_set( mode='OBJECT', toggle=False ) 234 | 235 | #move the mesh such that its new origin is the cursor position 236 | ao.data.transform( mathutils.Matrix.Translation( -cursor_pos_obj ) ) 237 | 238 | #move the obj to compensate for mesh translation 239 | delta = ao.matrix_world.to_3x3() @ cursor_pos_obj 240 | ao.location += delta 241 | 242 | for child in ao.children: 243 | child.location -= delta 244 | 245 | for inst in instances: 246 | inst_delta = inst.matrix_world.to_3x3() @ cursor_pos_obj 247 | inst.location += inst_delta 248 | for child in inst.children: 249 | child.location -= inst_delta 250 | 251 | bpy.ops.object.mode_set( mode=prev_mode, toggle=False ) 252 | 253 | return { 'FINISHED' } 254 | 255 | 256 | class VIEW3D_MT_PIE_cursor( bpy.types.Menu ): 257 | """A seriese of commands related to the 3D Cursor""" 258 | bl_idname = 'VIEW3D_MT_PIE_cursor' 259 | bl_label = '3D Cursor Ops' 260 | 261 | def draw( self, context ): 262 | layout = self.layout 263 | 264 | pie = layout.menu_pie() 265 | 266 | pie.operator( 'view3d.snap_selected_to_cursor', text='Selection to Cursor' ).use_offset = True 267 | 268 | pie.operator( 'view3d.snap_cursor_to_grid', text='Cursor to Grid' ) 269 | 270 | pie.operator( 'view3d.snap_cursor_to_selected', text='Cursor to Selection' ) 271 | 272 | pie.operator( 'view3d.rm_cursor_to_selection', text='Cursor to Selection and Orient' ) 273 | 274 | pie.operator( 'object.rm_origin_to_cursor', text='Object Pivot to Cursor' ) 275 | 276 | pie.operator( 'view3d.rm_zerocursor', text='Cursor to Origin' ) 277 | 278 | pie.operator( 'view3d.rm_unrotate_relative_to_cursor', text='Unrotate Relative to Cursor' ) 279 | 280 | pie.operator( 'wm.call_menu', text='Apply Transform' ).name = 'VIEW3D_MT_object_apply' 281 | 282 | 283 | def register(): 284 | bpy.utils.register_class( MESH_OT_cursortoselection ) 285 | bpy.utils.register_class( OBJECT_OT_origintocursor ) 286 | bpy.utils.register_class( VIEW3D_MT_PIE_cursor ) 287 | bpy.utils.register_class( MESH_OT_unrotatefromcursor ) 288 | 289 | 290 | def unregister(): 291 | bpy.utils.unregister_class( MESH_OT_cursortoselection ) 292 | bpy.utils.unregister_class( OBJECT_OT_origintocursor ) 293 | bpy.utils.unregister_class( VIEW3D_MT_PIE_cursor ) 294 | bpy.utils.unregister_class( MESH_OT_unrotatefromcursor ) -------------------------------------------------------------------------------- /dimensions.py: -------------------------------------------------------------------------------- 1 | import bpy, gpu, mathutils, blf 2 | from gpu_extras.batch import batch_for_shader 3 | from bpy_extras.view3d_utils import location_3d_to_region_2d 4 | import rmlib 5 | 6 | class DimensionsManager: 7 | shader = None 8 | batch = None 9 | handle = None 10 | handle_text = None 11 | active = False 12 | nodraw = False 13 | _joint = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 14 | _x_max = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 15 | _y_max = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 16 | _z_max = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 17 | _x_handle = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 18 | _y_handle = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 19 | _z_handle = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 20 | 21 | def __init__( self, context ): 22 | self.factor = 1.0 23 | DimensionsManager.shader = gpu.shader.from_builtin( 'POLYLINE_SMOOTH_COLOR' ) 24 | self.shader_batch() 25 | 26 | def update( self, context ): 27 | imperial = context.scene.unit_settings.system == 'IMPERIAL' 28 | length_unit = context.scene.unit_settings.length_unit 29 | self.factor = 1.0 30 | if imperial: 31 | if length_unit == 'INCHES': 32 | self.factor = 39.3701 33 | elif length_unit == 'FEET': 34 | self.factor = 3.28084 35 | elif length_unit == 'THOU': 36 | self.factor = 39370.1 37 | elif length_unit == 'MILES': 38 | self.factor = 0.000621371 39 | else: 40 | self.factor = 39.3701 41 | else: 42 | if length_unit == 'CENTIMETERS': 43 | self.factor = 100.0 44 | elif length_unit == 'KILOMETERS': 45 | self.factor = 1000.0 46 | elif length_unit == 'MILLIMETERS': 47 | self.factor = 0.01 48 | else: 49 | self.factor = 1.0 50 | 51 | self.shader_batch() 52 | try: 53 | DimensionsManager._x_handle = location_3d_to_region_2d( region=context.region, rv3d=context.region_data, coord=DimensionsManager._x_max ) 54 | DimensionsManager._y_handle = location_3d_to_region_2d( region=context.region, rv3d=context.region_data, coord=DimensionsManager._y_max ) 55 | DimensionsManager._z_handle = location_3d_to_region_2d( region=context.region, rv3d=context.region_data, coord=DimensionsManager._z_max ) 56 | except AttributeError: 57 | return 58 | 59 | for window in context.window_manager.windows: 60 | for area in window.screen.areas: 61 | if area.type == 'VIEW_3D': 62 | for region in area.regions: 63 | if region.type == 'WINDOW': 64 | region.tag_redraw() 65 | 66 | def shader_batch( self ): 67 | coords = [] 68 | coords.append( DimensionsManager._joint ) 69 | coords.append( DimensionsManager._x_max ) 70 | coords.append( DimensionsManager._joint ) 71 | coords.append( DimensionsManager._y_max ) 72 | coords.append( DimensionsManager._joint ) 73 | coords.append( DimensionsManager._z_max ) 74 | 75 | colors = [] 76 | colors.append( ( 1.0, 0.0, 0.0, 0.5 ) ) 77 | colors.append( ( 1.0, 0.0, 0.0, 0.5 ) ) 78 | colors.append( ( 0.0, 1.0, 0.0, 0.5 ) ) 79 | colors.append( ( 0.0, 1.0, 0.0, 0.5 ) ) 80 | colors.append( ( 0.0, 0.0, 1.0, 0.5 ) ) 81 | colors.append( ( 0.0, 0.0, 1.0, 0.5 ) ) 82 | 83 | content = { 'pos':coords, 'color':colors } 84 | DimensionsManager.batch = batch_for_shader( DimensionsManager.shader, 'LINES', content ) 85 | 86 | def draw( self ): 87 | if DimensionsManager.nodraw: 88 | return 89 | 90 | if DimensionsManager.batch: 91 | DimensionsManager.shader.bind() 92 | 93 | DimensionsManager.shader.uniform_float( 'lineWidth', 1 ) 94 | region = bpy.context.region 95 | DimensionsManager.shader.uniform_float( 'viewportSize', ( region.width, region.height ) ) 96 | 97 | DimensionsManager.batch.draw( DimensionsManager.shader ) 98 | 99 | def draw_text( self ): 100 | if DimensionsManager.nodraw or DimensionsManager._x_handle is None: 101 | return 102 | 103 | blf.color( 0, 1.0, 0.0, 0.0, 1.0 ) 104 | blf.position( 0, DimensionsManager._x_handle[0], DimensionsManager._x_handle[1], 0 ) 105 | blf.size( 0, 16 ) 106 | d = ( DimensionsManager._x_max - DimensionsManager._joint ).length 107 | d *= self.factor 108 | d = round( d, 4 ) 109 | blf.draw( 0, '{}'.format( d ) ) 110 | 111 | blf.color( 0, 0.0, 1.0, 0.0, 1.0 ) 112 | blf.position( 0, DimensionsManager._y_handle[0], DimensionsManager._y_handle[1], 0 ) 113 | blf.size( 0, 16 ) 114 | d = ( DimensionsManager._y_max - DimensionsManager._joint ).length 115 | d *= self.factor 116 | d = round( d, 4 ) 117 | blf.draw( 0, '{}'.format( d ) ) 118 | 119 | blf.color( 0, 0.0, 0.0, 1.0, 1.0 ) 120 | blf.position( 0, DimensionsManager._z_handle[0], DimensionsManager._z_handle[1], 0 ) 121 | blf.size( 0, 16 ) 122 | d = ( DimensionsManager._z_max - DimensionsManager._joint ).length 123 | d *= self.factor 124 | d = round( d, 4 ) 125 | blf.draw( 0, '{}'.format( d ) ) 126 | 127 | def doDraw( self ): 128 | DimensionsManager.handle = bpy.types.SpaceView3D.draw_handler_add( self.draw, (), 'WINDOW', 'POST_VIEW' ) 129 | DimensionsManager.handle_text = bpy.types.SpaceView3D.draw_handler_add( self.draw_text, (), 'WINDOW', 'POST_PIXEL' ) 130 | DimensionsManager.active = True 131 | 132 | def stopDraw( self, context ): 133 | try: 134 | bpy.types.SpaceView3D.draw_handler_remove( DimensionsManager.handle, 'WINDOW' ) 135 | bpy.types.SpaceView3D.draw_handler_remove( DimensionsManager.handle_text, 'WINDOW' ) 136 | except ValueError: 137 | pass 138 | DimensionsManager.active = False 139 | 140 | for window in context.window_manager.windows: 141 | for area in window.screen.areas: 142 | if area.type == 'VIEW_3D': 143 | for region in area.regions: 144 | if region.type == 'WINDOW': 145 | region.tag_redraw() 146 | 147 | 148 | def GetWorldSpaceBounds( rmmesh, bounds ): 149 | mat = rmmesh.world_transform 150 | min_p = bounds[0] 151 | max_p = bounds[1] 152 | 153 | corners = [] 154 | corners.append( min_p ) 155 | corners.append( mathutils.Vector( ( max_p[0], min_p[1], min_p[2] ) ) ) 156 | corners.append( mathutils.Vector( ( min_p[0], max_p[1], min_p[2] ) ) ) 157 | corners.append( mathutils.Vector( ( min_p[0], min_p[1], max_p[2] ) ) ) 158 | corners.append( mathutils.Vector( ( max_p[0], max_p[1], min_p[2] ) ) ) 159 | corners.append( mathutils.Vector( ( min_p[0], max_p[1], max_p[2] ) ) ) 160 | corners.append( mathutils.Vector( ( max_p[0], min_p[1], max_p[2] ) ) ) 161 | corners.append( max_p ) 162 | 163 | for i in range( 8 ): 164 | corners[i] = mat @ corners[i] 165 | 166 | min_p = mathutils.Vector( corners[0].copy() ) 167 | max_p = mathutils.Vector( corners[0].copy() ) 168 | for c in corners: 169 | for i in range( 3 ): 170 | if c[i] < min_p[i]: 171 | min_p[i] = c[i] 172 | if c[i] > max_p[i]: 173 | max_p[i] = c[i] 174 | 175 | return ( min_p, max_p ) 176 | 177 | 178 | BACKGROUND_LAYERNAME = 'rm_background' 179 | 180 | def GetSelsetPolygons( bm, layername ): 181 | intlayers = bm.faces.layers.int 182 | selset = intlayers.get( layername, None ) 183 | if selset is None: 184 | return rmlib.rmPolygonSet() 185 | return rmlib.rmPolygonSet( [ f for f in bm.faces if bool( f[selset] ) ] ) 186 | 187 | 188 | def GetBoundingBox( context ): 189 | bounding_box = None 190 | 191 | if context.mode == 'EDIT_MESH': 192 | sel_mode = context.tool_settings.mesh_select_mode[:] 193 | 194 | rmmesh = rmlib.rmMesh.GetActive( context ) 195 | if rmmesh is None: 196 | return bounding_box 197 | with rmmesh as rmmesh: 198 | rmmesh.readonly = True 199 | if sel_mode[0]: 200 | verts = rmlib.rmVertexSet.from_selection( rmmesh ) 201 | if len( verts ) == 0: 202 | verts = rmlib.rmVertexSet.from_mesh( rmmesh ) 203 | if len( verts ) == 0: 204 | return bounding_box 205 | elif sel_mode[1]: 206 | edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 207 | if len( edges ) == 0: 208 | edges = rmlib.rmEdgeSet.from_mesh( rmmesh ) 209 | if len( edges ) == 0: 210 | return bounding_box 211 | verts = edges.vertices 212 | 213 | if sel_mode[2] or context.scene.rmkit_props.dimensions_use_background_face_selection: 214 | if sel_mode[2]: 215 | faces = rmlib.rmPolygonSet.from_selection( rmmesh ) 216 | else: 217 | faces = GetSelsetPolygons( rmmesh.bmesh, BACKGROUND_LAYERNAME ) 218 | if len( faces ) == 0: 219 | faces = rmlib.rmPolygonSet.from_mesh( rmmesh ) 220 | if len( faces ) == 0: 221 | return bounding_box 222 | verts = faces.vertices 223 | 224 | min_p = mathutils.Vector( verts[0].co.copy() ) 225 | max_p = mathutils.Vector( verts[0].co.copy() ) 226 | for v in verts: 227 | for i in range( 3 ): 228 | if v.co[i] < min_p[i]: 229 | min_p[i] = v.co[i] 230 | if v.co[i] > max_p[i]: 231 | max_p[i] = v.co[i] 232 | 233 | bounding_box = GetWorldSpaceBounds( rmmesh, ( min_p, max_p ) ) 234 | 235 | 236 | elif context.mode == 'OBJECT': 237 | bbox_corners = [] 238 | for obj in context.selected_objects: 239 | if obj.type != 'MESH': 240 | continue 241 | bbox_corners += [ mathutils.Matrix( obj.matrix_world ) @ mathutils.Vector( t ) for t in obj.bound_box ] 242 | if len( bbox_corners ) == 0: 243 | bbox_corners = [ ( 0.0, 0.0, 0.0 ) ] 244 | 245 | min_p = mathutils.Vector( ( bbox_corners[0][0], bbox_corners[0][1], bbox_corners[0][2] ) ) 246 | max_p = mathutils.Vector( ( bbox_corners[0][0], bbox_corners[0][1], bbox_corners[0][2] ) ) 247 | for p in bbox_corners: 248 | for i in range( 3 ): 249 | if p[i] < min_p[i]: 250 | min_p[i] = p[i] 251 | if p[i] > max_p[i]: 252 | max_p[i] = p[i] 253 | 254 | bounding_box = ( min_p, max_p ) 255 | 256 | return bounding_box 257 | 258 | 259 | class MESH_OT_dimensions( bpy.types.Operator ): 260 | """Draw helpers in the viewport to visualize the dimensions of selected mesh elements.""" 261 | bl_idname = 'view3d.rm_dimensions' 262 | bl_label = 'Dimensions' 263 | 264 | DIMENSIONS_RENDER = None 265 | 266 | @classmethod 267 | def poll( cls, context ): 268 | return context.area.type == 'VIEW_3D' 269 | 270 | def invoke(self, context, event): 271 | #add a timer to modal 272 | wm = context.window_manager 273 | self._timer = wm.event_timer_add( 1.0 / 64.0, window=context.window ) 274 | wm.modal_handler_add( self ) 275 | 276 | self.execute( context ) 277 | 278 | return { 'RUNNING_MODAL' } 279 | 280 | def modal( self, context, event ): 281 | if not MESH_OT_dimensions.DIMENSIONS_RENDER.active: 282 | return { 'FINISHED' } 283 | 284 | if event.type == 'TIMER': 285 | DimensionsManager.nodraw = False 286 | if context.mode == 'OBJECT': 287 | objcount = 0 288 | for obj in context.selected_objects: 289 | if obj.type == 'MESH': 290 | objcount += 1 291 | if objcount == 0: 292 | DimensionsManager.nodraw = True 293 | return { 'PASS_THROUGH' } 294 | 295 | bounding_box = GetBoundingBox( context ) 296 | if bounding_box is None: 297 | DimensionsManager.nodraw = True 298 | return { 'PASS_THROUGH' } 299 | 300 | DimensionsManager._joint = bounding_box[0] 301 | DimensionsManager._x_max = mathutils.Vector( ( bounding_box[1][0], bounding_box[0][1], bounding_box[0][2] ) ) 302 | DimensionsManager._y_max = mathutils.Vector( ( bounding_box[0][0], bounding_box[1][1], bounding_box[0][2] ) ) 303 | DimensionsManager._z_max = mathutils.Vector( ( bounding_box[0][0], bounding_box[0][1], bounding_box[1][2] ) ) 304 | 305 | MESH_OT_dimensions.DIMENSIONS_RENDER.update( context ) 306 | 307 | return { 'PASS_THROUGH' } 308 | 309 | def execute( self, context ): 310 | if MESH_OT_dimensions.DIMENSIONS_RENDER is None: 311 | MESH_OT_dimensions.DIMENSIONS_RENDER = DimensionsManager( context ) 312 | 313 | if DimensionsManager.active: 314 | MESH_OT_dimensions.DIMENSIONS_RENDER.stopDraw( context ) 315 | return { 'FINISHED' } 316 | else: 317 | MESH_OT_dimensions.DIMENSIONS_RENDER.doDraw() 318 | 319 | return { 'FINISHED' } 320 | 321 | 322 | def register(): 323 | bpy.utils.register_class( MESH_OT_dimensions ) 324 | 325 | 326 | def unregister(): 327 | bpy.utils.unregister_class( MESH_OT_dimensions ) -------------------------------------------------------------------------------- /edgeweight.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh 2 | import rmlib 3 | 4 | def GetEdges( bmesh, sel_mode ): 5 | if sel_mode[1]: 6 | return rmlib.rmEdgeSet( [ e for e in bmesh.edges if e.select ] ) 7 | elif sel_mode[2]: 8 | edges = set() 9 | polys = rmlib.rmPolygonSet( [ f for f in bmesh.faces if f.select ] ) 10 | for e in polys.edges: 11 | for p in e.link_faces: 12 | if p not in polys: 13 | edges.add( e ) 14 | break 15 | return rmlib.rmEdgeSet( edges ) 16 | return rmlib.rmEdgeSet() 17 | 18 | def SetEdgeCrease( context, weight ): 19 | sel_mode = context.tool_settings.mesh_select_mode[:] 20 | rmmesh = rmlib.rmMesh.GetActive( context ) 21 | with rmmesh as rmmesh: 22 | rmmesh.skipchecks = True 23 | clyr = None 24 | if bpy.app.version < (4,0,0): 25 | c_layers = rmmesh.bmesh.edges.layers.crease 26 | clyr = c_layers.verify() 27 | else: 28 | clyr = rmmesh.bmesh.edges.layers.float.get( 'crease_edge', None ) 29 | if clyr is None: 30 | clyr = rmmesh.bmesh.edges.layers.float.new( 'crease_edge' ) 31 | for e in GetEdges( rmmesh.bmesh, sel_mode ): 32 | e[clyr] = weight 33 | 34 | 35 | def SetEdgeBevelWeight( context, weight ): 36 | sel_mode = context.tool_settings.mesh_select_mode[:] 37 | rmmesh = rmlib.rmMesh.GetActive( context ) 38 | with rmmesh as rmmesh: 39 | rmmesh.skipchecks = True 40 | blyr = None 41 | if bpy.app.version < (4,0,0): 42 | b_layers = rmmesh.bmesh.edges.layers.bevel_weight 43 | blyr = b_layers.verify() 44 | else: 45 | blyr = rmmesh.bmesh.edges.layers.float.get( 'bevel_weight_edge', None ) 46 | if blyr is None: 47 | blyr = rmmesh.bmesh.edges.layers.float.new( 'bevel_weight_edge' ) 48 | for e in GetEdges( rmmesh.bmesh, sel_mode ): 49 | e[blyr] = weight 50 | 51 | 52 | def SetEdgeSharp( context, weight ): 53 | sel_mode = context.tool_settings.mesh_select_mode[:] 54 | rmmesh = rmlib.rmMesh.GetActive( context ) 55 | with rmmesh as rmmesh: 56 | rmmesh.skipchecks = True 57 | for e in GetEdges( rmmesh.bmesh, sel_mode ): 58 | e.smooth = not bool( round( weight ) ) 59 | 60 | 61 | class MESH_OT_setedgeweight( bpy.types.Operator ): 62 | """Set create/bevelweight amount on selected edges.""" 63 | bl_idname = 'mesh.rm_setedgeweight' 64 | bl_label = 'Set Edge Weight' 65 | bl_options = { 'UNDO' } #tell blender that we support the undo/redo pannel 66 | 67 | weight_type: bpy.props.EnumProperty( 68 | items=[ ( "crease", "Crease", "", 1 ), 69 | ( "bevel_weight", "Bevel Weight", "", 2 ), 70 | ( "sharp", "Sharp", "", 3 ) ], 71 | name="Weight Type", 72 | default="crease" 73 | ) 74 | 75 | weight: bpy.props.FloatProperty( 76 | name='Weight', 77 | default=0.0 78 | ) 79 | 80 | @classmethod 81 | def poll( cls, context ): 82 | return ( context.area.type == 'VIEW_3D' and 83 | context.active_object is not None and 84 | context.active_object.type == 'MESH' and 85 | context.object.data.is_editmode ) 86 | 87 | def execute( self, context ): 88 | if context.object is None or context.mode == 'OBJECT': 89 | return { 'CANCELLED' } 90 | 91 | if self.weight_type == 'crease': 92 | SetEdgeCrease( context, self.weight ) 93 | elif self.weight_type == 'bevel_weight': 94 | SetEdgeBevelWeight( context, self.weight ) 95 | else: 96 | SetEdgeSharp( context, self.weight ) 97 | 98 | return { 'FINISHED' } 99 | 100 | 101 | class VIEW3D_MT_PIE_setedgeweight_crease( bpy.types.Menu ): 102 | """Set create/bevelweight amount on selected edges.""" 103 | bl_idname = 'VIEW3D_MT_PIE_setedgeweight_crease' 104 | bl_label = 'Edge Weight' 105 | 106 | def draw( self, context ): 107 | layout = self.layout 108 | 109 | pie = layout.menu_pie() 110 | 111 | op_w = pie.operator( MESH_OT_setedgeweight.bl_idname, text='100%' ) 112 | op_w.weight = 1.0 113 | op_w.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_crease 114 | 115 | op_e = pie.operator( MESH_OT_setedgeweight.bl_idname, text='30%' ) 116 | op_e.weight = 0.3 117 | op_e.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_crease 118 | 119 | op_s = pie.operator( MESH_OT_setedgeweight.bl_idname, text='60%' ) 120 | op_s.weight = 0.6 121 | op_s.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_crease 122 | 123 | op_n = pie.operator( MESH_OT_setedgeweight.bl_idname, text='0%' ) 124 | op_n.weight = 0.0 125 | op_n.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_crease 126 | 127 | pie.operator( 'wm.call_menu_pie', text='Bevel Weight' ).name = 'VIEW3D_MT_PIE_setedgeweight_bevel' 128 | 129 | op_ne = pie.operator( MESH_OT_setedgeweight.bl_idname, text='20%' ) 130 | op_ne.weight = 0.2 131 | op_ne.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_crease 132 | 133 | op_sw = pie.operator( MESH_OT_setedgeweight.bl_idname, text='80%' ) 134 | op_sw.weight = 0.8 135 | op_sw.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_crease 136 | 137 | op_se = pie.operator( MESH_OT_setedgeweight.bl_idname, text='40%' ) 138 | op_se.weight = 0.4 139 | op_se.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_crease 140 | 141 | 142 | class VIEW3D_MT_PIE_setedgeweight_bevel( bpy.types.Menu ): 143 | """Set create/bevelweight amount on selected edges.""" 144 | bl_idname = 'VIEW3D_MT_PIE_setedgeweight_bevel' 145 | bl_label = 'Edge Weight' 146 | 147 | def draw( self, context ): 148 | layout = self.layout 149 | 150 | pie = layout.menu_pie() 151 | 152 | op_w = pie.operator( MESH_OT_setedgeweight.bl_idname, text='100%' ) 153 | op_w.weight = 1.0 154 | op_w.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_bevel_weight 155 | 156 | op_e = pie.operator( MESH_OT_setedgeweight.bl_idname, text='30%' ) 157 | op_e.weight = 0.3 158 | op_e.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_bevel_weight 159 | 160 | op_s = pie.operator( MESH_OT_setedgeweight.bl_idname, text='60%' ) 161 | op_s.weight = 0.6 162 | op_s.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_bevel_weight 163 | 164 | op_n = pie.operator( MESH_OT_setedgeweight.bl_idname, text='0%' ) 165 | op_n.weight = 0.0 166 | op_n.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_bevel_weight 167 | 168 | pie.operator( 'wm.call_menu_pie', text='Crease' ).name = 'VIEW3D_MT_PIE_setedgeweight_crease' 169 | 170 | op_ne = pie.operator( MESH_OT_setedgeweight.bl_idname, text='20%' ) 171 | op_ne.weight = 0.2 172 | op_ne.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_bevel_weight 173 | 174 | op_sw = pie.operator( MESH_OT_setedgeweight.bl_idname, text='80%' ) 175 | op_sw.weight = 0.8 176 | op_sw.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_bevel_weight 177 | 178 | op_se = pie.operator( MESH_OT_setedgeweight.bl_idname, text='40%' ) 179 | op_se.weight = 0.4 180 | op_se.weight_type = context.scene.rmkit_props.edgeweightprops.ew_weight_type_bevel_weight 181 | 182 | 183 | def register(): 184 | bpy.utils.register_class( MESH_OT_setedgeweight ) 185 | bpy.utils.register_class( VIEW3D_MT_PIE_setedgeweight_crease ) 186 | bpy.utils.register_class( VIEW3D_MT_PIE_setedgeweight_bevel ) 187 | 188 | 189 | def unregister(): 190 | bpy.utils.unregister_class( MESH_OT_setedgeweight ) 191 | bpy.utils.unregister_class( VIEW3D_MT_PIE_setedgeweight_crease ) 192 | bpy.utils.unregister_class( VIEW3D_MT_PIE_setedgeweight_bevel ) -------------------------------------------------------------------------------- /extend.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh, mathutils 2 | import rmlib 3 | 4 | 5 | class MESH_OT_extend( bpy.types.Operator ): 6 | """Runs Extend Vert in vert mode, Edge Extude in edge mode, and DuplicateMode in face mode.""" 7 | bl_idname = 'mesh.rm_extend' 8 | bl_label = 'Extend' 9 | bl_options = { 'UNDO' } 10 | 11 | @classmethod 12 | def poll( cls, context ): 13 | return context.area.type == 'VIEW_3D' 14 | 15 | def execute( self, context ): 16 | #get the selection mode 17 | if context.object is None: 18 | return { 'CANCELLED' } 19 | 20 | if context.mode == 'OBJECT': 21 | bpy.ops.object.duplicate_move_linked( 'INVOKE_DEFAULT' ) 22 | return { 'FINISHED' } 23 | 24 | 25 | if context.object.type != 'MESH': 26 | return { 'CANCELLED' } 27 | 28 | sel_mode = context.tool_settings.mesh_select_mode[:] 29 | if sel_mode[0]: 30 | use_extrude = True 31 | rmmesh = rmlib.rmMesh.GetActive( context ) 32 | if rmmesh is None: 33 | return { 'CANCELLED' } 34 | with rmmesh as rmmesh: 35 | rmmesh.readonly = True 36 | vert_selection = rmlib.rmVertexSet.from_selection( rmmesh ) 37 | if len( vert_selection ) == 0: 38 | return { 'CANCELLED' } 39 | for e in vert_selection.edges: 40 | if len( list( e.link_faces ) ) > 0: 41 | use_extrude = False 42 | break 43 | if use_extrude: 44 | bpy.ops.mesh.extrude_vertices_move( 'INVOKE_DEFAULT' ) 45 | else: 46 | bpy.ops.mesh.rip_edge_move( 'INVOKE_DEFAULT' ) 47 | 48 | elif sel_mode[1]: 49 | bpy.ops.mesh.extrude_edges_move( 'INVOKE_DEFAULT' ) 50 | elif sel_mode[2]: 51 | bpy.ops.mesh.duplicate_move( 'INVOKE_DEFAULT' ) 52 | else: 53 | return { 'CANCELLED' } 54 | 55 | return { 'FINISHED' } 56 | 57 | 58 | def register(): 59 | bpy.utils.register_class( MESH_OT_extend ) 60 | 61 | 62 | def unregister(): 63 | bpy.utils.unregister_class( MESH_OT_extend ) -------------------------------------------------------------------------------- /extrudealongpath.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh, mathutils 2 | import rmlib 3 | 4 | BACKGROUND_LAYERNAME = 'rm_background' 5 | 6 | def GetSelsetEdges( bm, layername ): 7 | intlayers = bm.edges.layers.int 8 | selset = intlayers.get( layername, None ) 9 | if selset is None: 10 | return rmlib.rmEdgeSet() 11 | return rmlib.rmEdgeSet( [ e for e in bm.edges if bool( e[selset] ) ] ) 12 | 13 | 14 | class MESH_OT_extrudealongpath( bpy.types.Operator ): 15 | """Extrude the face selection along the path defined by the background edge selection.""" 16 | bl_idname = 'mesh.rm_extrudealongpath' 17 | bl_label = 'Extrude Along Path' 18 | bl_options = { 'UNDO' } 19 | 20 | offsetonly: bpy.props.BoolProperty( 21 | name='Offset Only', 22 | default=False 23 | ) 24 | 25 | centerprofileonpath: bpy.props.BoolProperty( 26 | name= 'Center Profile On Path', 27 | default=False 28 | ) 29 | 30 | @classmethod 31 | def poll( cls, context ): 32 | return ( context.area.type == 'VIEW_3D' and 33 | context.active_object is not None and 34 | context.active_object.type == 'MESH' and 35 | context.object.data.is_editmode ) 36 | 37 | def execute( self, context ): 38 | if context.object is None or context.mode == 'OBJECT': 39 | return { 'CANCELLED' } 40 | 41 | if context.object.type != 'MESH': 42 | return { 'CANCELLED' } 43 | 44 | sel_mode = context.tool_settings.mesh_select_mode[:] 45 | if not sel_mode[2]: 46 | self.report( { 'WARNING' }, 'Must be in face mode.' ) 47 | return { 'CANCELLED' } 48 | 49 | rmmesh = rmlib.rmMesh.GetActive( context ) 50 | with rmmesh as rmmesh: 51 | # Get background edge selection set 52 | selset_edges = GetSelsetEdges( rmmesh.bmesh, BACKGROUND_LAYERNAME ) 53 | if len( selset_edges ) < 1: 54 | self.report( { 'WARNING' }, 'Must have background edges selected on current active mesh. Use \"Change/Convert Mode To\" ops provided by addon.' ) 55 | return { 'CANCELLED' } 56 | 57 | # Get currently selected faces 58 | selected_faces = rmlib.rmPolygonSet.from_selection( rmmesh ) 59 | if len( selected_faces ) < 1: 60 | self.report( { 'ERROR' }, 'Must have at least one face selected!!!' ) 61 | return { 'CANCELLED' } 62 | 63 | new_faces = rmlib.rmPolygonSet() 64 | # Iterate over each chain of selected path edges 65 | for path_edges in selset_edges.chain(): 66 | if len( path_edges ) < 1: 67 | continue 68 | 69 | # For each group of selected faces 70 | for group in selected_faces.group(): 71 | boundary_edges = rmlib.rmEdgeSet() 72 | # Find boundary edges of the face group 73 | for e in group.edges: 74 | linkfaces = list( e.link_faces ) 75 | if len( linkfaces ) <= 1: 76 | boundary_edges.append( e ) 77 | continue 78 | 79 | selfacecount = 0 80 | for f in e.link_faces: 81 | if f.select: 82 | selfacecount += 1 83 | if selfacecount == 1: 84 | boundary_edges.append( e ) 85 | continue 86 | 87 | try: 88 | # Get ordered chain of boundary vertices 89 | vchain = boundary_edges.vert_chain()[0] 90 | except IndexError: 91 | continue 92 | 93 | # Copy the profile (boundary) vertex coordinates 94 | profile = [ v.co.copy() for v in vchain ] 95 | 96 | # Compute the center of the profile 97 | profile_center = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 98 | for p in profile: 99 | profile_center += p 100 | profile_center /= len( profile ) 101 | 102 | # Determine if the path is closed 103 | closed_path = path_edges[0][0] == path_edges[-1][-1] 104 | 105 | # Compute the average normal of the selected faces 106 | profile_nml = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 107 | for f in selected_faces: 108 | profile_nml += f.normal 109 | profile_nml.normalized() 110 | 111 | if closed_path: 112 | # For closed paths, align the profile normal with the path direction 113 | max_dot = 0.0 114 | max_idx = 0 115 | for i, path_edge in enumerate( path_edges ): 116 | dot = profile_nml.dot( ( path_edge[0].co - path_edge[1].co ).normalized() ) 117 | if abs( dot ) > abs( max_dot ): 118 | max_dot = dot 119 | max_idx = i 120 | path_edges = path_edges[max_idx:] + path_edges[:max_idx] 121 | if max_dot < 0.0: 122 | profile = profile[::-1] 123 | else: 124 | # For open paths, ensure profile orientation matches path direction 125 | if ( profile_center - path_edges[0][0].co ).length > ( profile_center - path_edges[-1][-1].co ).length: 126 | path_edges = path_edges[::-1] 127 | for i, t in enumerate( path_edges ): 128 | path_edges[i] = ( t[1], t[0] ) 129 | vec = ( path_edges[0][1].co - path_edges[0][0].co ).normalized() 130 | if vec.dot( profile_nml ) < 0.0: 131 | profile = profile[::-1] 132 | 133 | # Project the profile onto the plane defined by the first path segment 134 | first_path_v1, first_path_v2 = path_edges[0] 135 | if closed_path: 136 | vec1 = ( first_path_v2.co - first_path_v1.co ).normalized() 137 | vec2 = ( first_path_v1.co - path_edges[-1][0].co ).normalized() 138 | plane_nml = vec1 + vec2 139 | if plane_nml.length < rmlib.util.FLOAT_EPSILON: 140 | plane_nml = vec1 141 | plane_nml.normalize() 142 | else: 143 | plane_nml = ( first_path_v2.co.copy() - first_path_v1.co.copy() ).normalized() 144 | 145 | # Move each profile vertex onto the plane 146 | if not self.centerprofileonpath: 147 | profile_center = path_edges[0][0].co.copy() 148 | for i in range( len( profile ) ): 149 | p = profile[i] 150 | dist = rmlib.util.PlaneDistance( p, profile_center, plane_nml ) 151 | profile[i] = p - plane_nml * dist 152 | for i, v in enumerate( vchain ): 153 | v.co = profile[i] 154 | 155 | profile_center_accumulated_average = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 156 | # Create new vertices for the profile 157 | profile_verts = rmlib.rmVertexSet( [ rmmesh.bmesh.verts.new( v.co ) for v in vchain ] ) 158 | first_profile = profile_verts 159 | extruded_faces = rmlib.rmPolygonSet() 160 | # Extrude the profile along each path segment 161 | for n, path_edge in enumerate( path_edges ): 162 | v1, v2 = path_edge 163 | pos1 = v1.co.copy() 164 | pos2 = v2.co.copy() 165 | offset = pos2 - pos1 166 | 167 | if not self.centerprofileonpath: 168 | profile_center = pos1 169 | 170 | profile_center_accumulated_average += profile_center 171 | profile_center += offset 172 | 173 | plane_nml = mathutils.Vector( ( 1.0, 0.0, 0.0 ) ) 174 | if not self.offsetonly: 175 | try: 176 | v1, v2 = path_edges[n+1] 177 | except IndexError: 178 | pass 179 | pos1 = v1.co.copy() 180 | pos2 = v2.co.copy() 181 | 182 | vec_a = offset.normalized() 183 | vec_b = ( pos2 - pos1 ).normalized() 184 | plane_nml = vec_a + vec_b 185 | if plane_nml.length < rmlib.util.FLOAT_EPSILON: 186 | plane_nml = vec_a 187 | plane_nml.normalize() 188 | 189 | if closed_path and n == len( path_edges ) - 1: 190 | # For closed paths, connect the last profile to the first 191 | for i in range( len( profile ) ): 192 | vlist = [ profile_verts[i-1], profile_verts[i], first_profile[i], first_profile[i-1] ] 193 | face = rmmesh.bmesh.faces.new( vlist, group[0] ) 194 | extruded_faces.append( face ) 195 | else: 196 | # For open paths, create new profile and faces for each segment 197 | new_profile = [None] * len( profile ) 198 | new_profile_verts = [None] * len( profile ) 199 | for i in range( len( profile ) ): 200 | if self.offsetonly: 201 | new_profile[i] = profile[i] + offset 202 | else: 203 | new_profile[i] = mathutils.geometry.intersect_line_plane( profile[i], profile[i] + offset * 10.0, profile_center, plane_nml ) 204 | 205 | if new_profile[i] is None: 206 | new_profile[i] = profile[i] + offset 207 | new_profile_verts[i] = rmmesh.bmesh.verts.new( new_profile[i] ) 208 | 209 | for i in range( len( profile ) ): 210 | vlist = [ profile_verts[i-1], profile_verts[i], new_profile_verts[i], new_profile_verts[i-1] ] 211 | face = rmmesh.bmesh.faces.new( vlist, group[0] ) 212 | extruded_faces.append( face ) 213 | 214 | profile_verts = new_profile_verts 215 | profile = new_profile 216 | 217 | if self.centerprofileonpath: 218 | # Move extruded faces so they align with the path 219 | path_average = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 220 | for v1, v2 in path_edges: 221 | path_average += v1.co 222 | if not closed_path: 223 | profile_center_accumulated_average += profile_center 224 | profile_center_accumulated_average /= len( path_edges ) + 1 225 | path_average += v2.co 226 | path_average /= len( path_edges ) + 1 227 | else: 228 | profile_center_accumulated_average /= len( path_edges ) 229 | path_average /= len( path_edges ) 230 | delta = path_average - profile_center_accumulated_average 231 | for v in extruded_faces.vertices: 232 | v.co += delta 233 | new_faces += extruded_faces 234 | 235 | # Delete the original selected faces and select the new extruded faces 236 | bmesh.ops.delete( rmmesh.bmesh, geom=selected_faces, context='FACES' ) 237 | new_faces.select( replace=True ) 238 | # Optionally gridify UVs for the new geometry 239 | bpy.ops.mesh.rm_uvgridify() 240 | 241 | return { 'FINISHED' } 242 | 243 | def invoke(self, context, event): 244 | # Show a popup dialog with the operator's properties 245 | return context.window_manager.invoke_props_dialog(self) 246 | 247 | def draw(self, context): 248 | layout = self.layout 249 | layout.prop(self, "offsetonly") 250 | layout.prop(self, "centerprofileonpath") 251 | 252 | 253 | def register(): 254 | bpy.utils.register_class( MESH_OT_extrudealongpath ) 255 | 256 | 257 | def unregister(): 258 | bpy.utils.unregister_class( MESH_OT_extrudealongpath ) -------------------------------------------------------------------------------- /grabapplymat.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh, mathutils, bpy_extras 2 | import rmlib 3 | 4 | def ResetSubdivModLevels( mods ): 5 | for mod, level in mods.items(): 6 | mod.levels = level 7 | 8 | class MESH_OT_grabapplymat( bpy.types.Operator ): 9 | """Sample the material of the face under the cursor and apply it to the selected faces.""" 10 | bl_idname = 'mesh.rm_grabapplymat' 11 | bl_label = 'GrabApplyMat (MOS)' 12 | bl_options = { 'UNDO' } #tell blender that we support the undo/redo pannel 13 | 14 | @classmethod 15 | def poll( cls, context ): 16 | return ( context.area.type == 'VIEW_3D' and 17 | context.active_object is not None and 18 | context.active_object.type == 'MESH' and 19 | context.object.data.is_editmode ) 20 | 21 | def GrabApplyEdgeWeight( self, context ): 22 | mouse_pos = mathutils.Vector( ( float( self.m_x ), float( self.m_y ) ) ) 23 | 24 | target_rmmesh_list = [] 25 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 26 | target_rmmesh_list.append( rmmesh ) 27 | 28 | mos_rmmesh = rmlib.rmMesh.from_mos( context, mouse_pos ) #used to get mat data. using eval mat data causes crash 29 | if mos_rmmesh is None: 30 | return { 'CANCELLED' } 31 | 32 | mos_edge_seam = None 33 | mos_edge_smooth = None 34 | bevel_weight = None 35 | crease_weight = None 36 | with mos_rmmesh as mos_rmmesh: 37 | mos_rmmesh.readonly = True 38 | 39 | mos_edges = rmlib.rmEdgeSet.from_mos( mos_rmmesh, context, mouse_pos, pixel_radius=8 ) 40 | if len( mos_edges ) < 1: 41 | return { 'CANCELLED' } 42 | 43 | mos_edge_seam = mos_edges[0].seam 44 | mos_edge_smooth = mos_edges[0].smooth 45 | 46 | if bpy.app.version < (4,0,0): 47 | bevlayers = mos_rmmesh.bmesh.edges.layers.bevel_weight 48 | try: 49 | bev_layer = bevlayers.items()[0] 50 | bevel_weight = mos_edges[0][bev_layer[1]] 51 | except IndexError: 52 | pass 53 | 54 | crslayers = mos_rmmesh.bmesh.edges.layers.crease 55 | try: 56 | crs_layer = crslayers.items()[0] 57 | crease_weight = mos_edges[0][crs_layer[1]] 58 | except IndexError: 59 | pass 60 | else: 61 | bev_layer = mos_rmmesh.bmesh.edges.layers.float.get( 'bevel_weight_edge', None ) 62 | if bev_layer is not None: 63 | bevel_weight = mos_edges[0][bev_layer] 64 | 65 | crs_layer = mos_rmmesh.bmesh.edges.layers.float.get( 'crease_edge', None ) 66 | if crs_layer is not None: 67 | crease_weight = mos_edges[0][crs_layer] 68 | 69 | blyr = None 70 | clyr = None 71 | for rmmesh in target_rmmesh_list: 72 | bm = bmesh.from_edit_mesh( rmmesh.mesh ) 73 | rmmesh.bmesh = bm 74 | 75 | if bpy.app.version < (4,0,0): 76 | if bevel_weight is not None: 77 | b_layers = rmmesh.bmesh.edges.layers.bevel_weight 78 | blyr = b_layers.verify() 79 | 80 | if crease_weight is not None: 81 | c_layers = rmmesh.bmesh.edges.layers.crease 82 | clyr = c_layers.verify() 83 | else: 84 | blyr = rmmesh.bmesh.edges.layers.float.get( 'bevel_weight_edge', None ) 85 | if blyr is None: 86 | blyr = rmmesh.bmesh.edges.layers.float.new( 'bevel_weight_edge' ) 87 | 88 | clyr = rmmesh.bmesh.edges.layers.float.get( 'crease_edge', None ) 89 | if clyr is None: 90 | clyr = rmmesh.bmesh.edges.layers.float.new( 'crease_edge' ) 91 | 92 | edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 93 | for e in edges: 94 | if mos_edge_seam is not None: 95 | e.seam = mos_edge_seam 96 | if mos_edge_smooth is not None: 97 | e.smooth = mos_edge_smooth 98 | if blyr is not None and bevel_weight is not None: 99 | e[blyr] = bevel_weight 100 | if clyr is not None and crease_weight is not None: 101 | e[clyr] = crease_weight 102 | 103 | bmesh.update_edit_mesh( rmmesh.mesh, loop_triangles=False, destructive=False ) 104 | 105 | return { 'FINISHED' } 106 | 107 | def GrabApplyMat( self, context ): 108 | mouse_pos = mathutils.Vector( ( float( self.m_x ), float( self.m_y ) ) ) 109 | 110 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 111 | bm = bmesh.from_edit_mesh( rmmesh.mesh ) 112 | rmmesh.bmesh = bm 113 | 114 | faces = rmlib.rmPolygonSet.from_selection( rmmesh ) 115 | if len( faces ) < 1: 116 | return { 'CANCELLED' } 117 | 118 | #get obj 119 | mos_rmmesh = rmlib.rmMesh.from_mos( context, mouse_pos ) #used to get mat data. using eval mat data causes crash 120 | if mos_rmmesh is None: 121 | return { 'CANCELLED' } 122 | 123 | #cache the levels of each subdiv mod for the mos_rmmesh and set their levels to 0 124 | subdiv_mods = {} 125 | for mod in mos_rmmesh.object.modifiers: 126 | if mod.type == 'SUBSURF': 127 | subdiv_mods[mod] = mod.levels 128 | mod.levels = 0 129 | 130 | eval_rmmesh = mos_rmmesh.GetEvaluated( context ) #used to get mos polygon after modifiers and anims 131 | 132 | #get the material index from the evaluated mesh under the mouse 133 | with eval_rmmesh as eval_rmmesh: 134 | eval_rmmesh.readonly = True 135 | try: 136 | source_poly = rmlib.rmPolygonSet.from_mos( eval_rmmesh, context, mouse_pos, ignore_hidden=eval_rmmesh.mesh.is_editmode )[0] 137 | except IndexError: 138 | ResetSubdivModLevels( subdiv_mods ) 139 | print( 'ERROR :: GrabApplyMat :: from_mos failed' ) 140 | return { 'CANCELLED' } 141 | source_mat_idx = source_poly.material_index 142 | 143 | if len( mos_rmmesh.mesh.materials ) < 1: 144 | ResetSubdivModLevels( subdiv_mods ) 145 | self.report( { 'WARNING' }, 'Material under cursor is emply!!!' ) 146 | return { 'CANCELLED' } 147 | 148 | #apply material 149 | match_found = False 150 | for i, mat in enumerate( rmmesh.object.data.materials ): 151 | if mat == mos_rmmesh.mesh.materials[source_mat_idx]: 152 | match_found = True 153 | for f in faces: 154 | f.material_index = i 155 | break 156 | if not match_found: 157 | rmmesh.object.data.materials.append( mos_rmmesh.mesh.materials[source_mat_idx] ) 158 | for f in faces: 159 | f.material_index = len( rmmesh.object.data.materials ) - 1 160 | 161 | bmesh.update_edit_mesh( rmmesh.mesh, loop_triangles=False, destructive=False ) 162 | 163 | ResetSubdivModLevels( subdiv_mods ) 164 | 165 | return { 'FINISHED' } 166 | 167 | def execute( self, context ): 168 | if context.object is None or context.mode == 'OBJECT': 169 | return { 'CANCELLED' } 170 | 171 | sel_mode = context.tool_settings.mesh_select_mode[:] 172 | if sel_mode[1]: 173 | return self.GrabApplyEdgeWeight( context ) 174 | elif sel_mode[2]: 175 | return self.GrabApplyMat( context ) 176 | 177 | return { 'CANCELLED' } 178 | 179 | def invoke( self, context, event ): 180 | self.m_x, self.m_y = event.mouse_region_x, event.mouse_region_y 181 | return self.execute( context ) 182 | 183 | 184 | def GetSelectedLoops( context, rmmesh ): 185 | selected_loops = [] 186 | 187 | sel_mode = context.tool_settings.mesh_select_mode[:] 188 | if sel_mode[0]: 189 | verts = rmlib.rmVertexSet.from_selection( rmmesh ) 190 | for v in verts: 191 | selected_loops += list( v.link_loops ) 192 | elif sel_mode[1]: 193 | edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 194 | for v in edges.vertices: 195 | selected_loops += list( v.link_loops ) 196 | elif sel_mode[2]: 197 | faces = rmlib.rmPolygonSet.from_selection( rmmesh ) 198 | for f in faces: 199 | selected_loops += list( faces.loops ) 200 | 201 | return selected_loops 202 | 203 | 204 | class MESH_OT_grabapplyvcolor( bpy.types.Operator ): 205 | """Sample the vcolors of the vert under the cursor and apply it to the selected faces.""" 206 | bl_idname = 'mesh.rm_grabapplyvcolor' 207 | bl_label = 'GrabApplyVertColor (MOS)' 208 | bl_options = { 'UNDO' } #tell blender that we support the undo/redo pannel 209 | 210 | @classmethod 211 | def poll( cls, context ): 212 | return ( context.area.type == 'VIEW_3D' and 213 | context.active_object is not None and 214 | context.active_object.type == 'MESH' and 215 | context.object.data.is_editmode ) 216 | 217 | def GrabApplyVColor( self, context ): 218 | mouse_pos = mathutils.Vector( ( float( self.m_x ), float( self.m_y ) ) ) 219 | 220 | active_items = [] 221 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 222 | with rmmesh as rmmesh: 223 | rmmesh.readonly = True 224 | selected_loops = GetSelectedLoops( context, rmmesh ) 225 | if len( selected_loops ) > 0: 226 | active_items.append( rmmesh ) 227 | if len( active_items ) == 0: 228 | self.report( { 'ERROR' }, 'No selected elems!!!' ) 229 | return { 'CANCELLED' } 230 | 231 | #get obj 232 | mos_rmmesh = rmlib.rmMesh.from_mos( context, mouse_pos ) #used to get mat data. using eval mat data causes crash 233 | if mos_rmmesh is None: 234 | self.report( { 'ERROR' }, 'No MOS mesh!!!' ) 235 | return { 'CANCELLED' } 236 | 237 | #cache the levels of each subdiv mod for the mos_rmmesh and set their levels to 0 238 | subdiv_mods = {} 239 | for mod in mos_rmmesh.object.modifiers: 240 | if mod.type == 'SUBSURF': 241 | subdiv_mods[mod] = mod.levels 242 | mod.levels = 0 243 | 244 | eval_rmmesh = mos_rmmesh.GetEvaluated( context ) #used to get mos polygon after modifiers and anims 245 | 246 | #get the material index from the evaluated mesh under the mouse 247 | with eval_rmmesh as eval_rmmesh: 248 | eval_rmmesh.readonly = True 249 | 250 | sourcelayerdata = {} 251 | try: 252 | source_face = rmlib.rmPolygonSet.from_mos( eval_rmmesh, context, mouse_pos )[0] 253 | source_vert = rmlib.rmVertexSet.from_mos( eval_rmmesh, context, mouse_pos, nearest=True, filter_verts=list( source_face.verts ) )[0] 254 | source_loop = None 255 | for l in source_face.loops: 256 | if l.vert == source_vert: 257 | source_loop = l 258 | break 259 | if source_loop is None: 260 | ResetSubdivModLevels( subdiv_mods ) 261 | self.report( { 'ERROR' }, 'Could not find MOS loop.' ) 262 | return { 'CANCELLED' } 263 | for lyr in eval_rmmesh.bmesh.loops.layers.color: 264 | sourcelayerdata[lyr.name] = source_loop[lyr].copy() 265 | except IndexError: 266 | ResetSubdivModLevels( subdiv_mods ) 267 | self.report( { 'WARNING' }, 'GrabApplyVColor :: from_mos failed' ) 268 | return { 'CANCELLED' } 269 | 270 | if len( sourcelayerdata ) < 1: 271 | ResetSubdivModLevels( subdiv_mods ) 272 | self.report( { 'WARNING' }, 'Could not find VertexColors under cursor!!!' ) 273 | return { 'CANCELLED' } 274 | 275 | for rmmesh in active_items: 276 | with rmmesh as rmmesh: 277 | #apply vertexcolor 278 | for srclyrname, layervalue in sourcelayerdata.items(): 279 | targlyrfound = False 280 | for targlyr in rmmesh.bmesh.loops.layers.color: 281 | if targlyr.name == srclyrname: 282 | targlyrfound = True 283 | break 284 | if not targlyrfound: 285 | targlyr = rmmesh.bmesh.loops.layers.color.new( srclyrname ) 286 | 287 | selected_loops = GetSelectedLoops( context, rmmesh ) 288 | if len( selected_loops ) == 0: 289 | continue 290 | for l in selected_loops: 291 | l[targlyr] = layervalue 292 | 293 | bmesh.update_edit_mesh( rmmesh.mesh, loop_triangles=False, destructive=False ) 294 | 295 | ResetSubdivModLevels( subdiv_mods ) 296 | 297 | return { 'FINISHED' } 298 | 299 | def execute( self, context ): 300 | if context.object is None or context.mode == 'OBJECT': 301 | return { 'CANCELLED' } 302 | 303 | self.GrabApplyVColor( context ) 304 | 305 | return { 'CANCELLED' } 306 | 307 | def invoke( self, context, event ): 308 | self.m_x, self.m_y = event.mouse_region_x, event.mouse_region_y 309 | return self.execute( context ) 310 | 311 | 312 | def register(): 313 | bpy.utils.register_class( MESH_OT_grabapplymat ) 314 | bpy.utils.register_class( MESH_OT_grabapplyvcolor ) 315 | 316 | 317 | def unregister(): 318 | bpy.utils.unregister_class( MESH_OT_grabapplymat ) 319 | bpy.utils.unregister_class( MESH_OT_grabapplyvcolor ) -------------------------------------------------------------------------------- /knifescreen.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh, mathutils 2 | import rmlib 3 | 4 | BACKGROUND_LAYERNAME = 'rm_background' 5 | 6 | def GetSelsetPolygons( bm, layername ): 7 | intlayers = bm.faces.layers.int 8 | selset = intlayers.get( layername, None ) 9 | if selset is None: 10 | return rmlib.rmPolygonSet() 11 | return rmlib.rmPolygonSet( [ f for f in bm.faces if bool( f[selset] ) ] ) 12 | 13 | 14 | class MESH_OT_knifescreen( bpy.types.Operator ): 15 | """Slice the background face selection based on the current vert/edge selection.""" 16 | bl_idname = 'mesh.rm_knifescreen' 17 | bl_label = 'KnifeScreen' 18 | bl_options = { 'UNDO' } 19 | 20 | str_dir: bpy.props.EnumProperty( 21 | items=[ ( "horizontal", "Horizontal", "", 1 ), 22 | ( "vertical", "Vertical", "", 2 ), 23 | ( "edge", "Edge", "", 3 ) ], 24 | name="Direction", 25 | default="horizontal" 26 | ) 27 | 28 | alignment: bpy.props.EnumProperty( 29 | items=[ ( "topology", "Topology", "", 1 ), 30 | ( "grid", "Grid", "", 2 ), 31 | ( "screen", "Screen", "", 3 ) ], 32 | name="Alignment", 33 | default="topology" 34 | ) 35 | 36 | mouse_pos: bpy.props.FloatVectorProperty( 37 | name="Cursor Position", 38 | size=2, 39 | default=( 0.0, 0.0 ) 40 | ) 41 | 42 | @classmethod 43 | def poll( cls, context ): 44 | return ( context.area.type == 'VIEW_3D' and 45 | context.active_object is not None and 46 | context.active_object.type == 'MESH' and 47 | context.object.data.is_editmode ) 48 | 49 | def execute( self, context ): 50 | if context.object is None or context.mode == 'OBJECT': 51 | return { 'CANCELLED' } 52 | 53 | if context.object.type != 'MESH': 54 | return { 'CANCELLED' } 55 | 56 | rm_vp = rmlib.rmViewport( context ) 57 | rm_wp = rmlib.rmCustomOrientation.from_selection( context ) 58 | 59 | sel_mode = context.tool_settings.mesh_select_mode[:] 60 | if sel_mode[2]: 61 | return { 'CANCELLED' } 62 | 63 | rmmesh = rmlib.rmMesh.GetActive( context ) 64 | with rmmesh as rmmesh: 65 | #init geom list for slicing 66 | active_polys = GetSelsetPolygons( rmmesh.bmesh, BACKGROUND_LAYERNAME ) 67 | if len( active_polys ) < 1: 68 | return { 'CANCELLED' } 69 | 70 | geom = [] 71 | geom.extend( active_polys.edges ) 72 | geom.extend( active_polys ) 73 | 74 | inv_rot_mat = rmmesh.world_transform.to_3x3().inverted() 75 | 76 | #in vert mode, slice active polys horizontally or vertically 77 | if sel_mode[0]: 78 | selected_vertices = rmlib.rmVertexSet.from_selection( rmmesh ) 79 | if len( selected_vertices ) < 1: 80 | return { 'CANCELLED' } 81 | 82 | for v in selected_vertices: 83 | plane_pos = v.co 84 | if self.alignment == 'topology': 85 | dir_idx, cam_dir_vec, grid_dir_vec = rm_vp.get_nearest_direction_vector( self.str_dir, rm_wp.matrix ) 86 | vnorm = mathutils.Vector( v.normal ) 87 | grid_dir_vec = inv_rot_mat @ grid_dir_vec 88 | plane_nml = grid_dir_vec.cross( vnorm ) 89 | elif self.alignment == 'grid': 90 | strdir = 'horizontal' 91 | if self.str_dir == 'horizontal': 92 | strdir = 'vertical' 93 | dir_idx, cam_dir_vec, plane_nml = rm_vp.get_nearest_direction_vector( strdir, rm_wp.matrix ) 94 | plane_nml = inv_rot_mat @ plane_nml 95 | else: 96 | strdir = 'horizontal' 97 | if self.str_dir == 'horizontal': 98 | strdir = 'vertical' 99 | dir_idx, plane_nml, grid_dir_vec = rm_vp.get_nearest_direction_vector( self.str_dir, rm_wp.matrix ) 100 | plane_nml = inv_rot_mat @ plane_nml 101 | 102 | #slice op 103 | d = bmesh.ops.bisect_plane( rmmesh.bmesh, geom=geom, dist=0.00001, plane_co=plane_pos, plane_no=plane_nml, use_snap_center=False, clear_outer=False, clear_inner=False ) 104 | geom = d['geom'] 105 | 106 | #in edge mode, slice active polys along edges 107 | elif sel_mode[1]: 108 | selected_edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 109 | if len( selected_edges ) < 1: 110 | return { 'CANCELLED' } 111 | 112 | for e in selected_edges: 113 | plane_pos = e.verts[0].co 114 | 115 | edge_vec = ( e.verts[0].co - e.verts[1].co ).normalized() 116 | if self.alignment == 'topology': 117 | edge_nml = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 118 | for f in e.link_faces: 119 | edge_nml += f.normal 120 | edge_nml.normalize() 121 | plane_nml = edge_vec.cross( edge_nml ) 122 | elif self.alignment == 'grid': 123 | dir_idx, cam_dir_vec, plane_nml = rm_vp.get_nearest_direction_vector( 'front', rm_wp.matrix ) 124 | plane_nml = inv_rot_mat @ plane_nml 125 | plane_nml = plane_nml.cross( edge_vec ) 126 | else: 127 | dir_idx, plane_nml, grid_dir_vec = rm_vp.get_nearest_direction_vector( 'front', rm_wp.matrix ) 128 | plane_nml = inv_rot_mat @ plane_nml 129 | plane_nml = plane_nml.cross( edge_vec ) 130 | 131 | #slice op 132 | d = bmesh.ops.bisect_plane( rmmesh.bmesh, geom=geom, dist=0.00001, plane_co=plane_pos, plane_no=plane_nml, use_snap_center=False, clear_outer=False, clear_inner=False ) 133 | geom = d['geom'] 134 | 135 | return { 'FINISHED' } 136 | 137 | 138 | class VIEW3D_MT_knifescreen( bpy.types.Menu ): 139 | """Slice the background face selection based on the current vert/edge selection.""" 140 | bl_idname = 'OBJECT_MT_rm_knifescreen' 141 | bl_label = 'Knife Screen GUI' 142 | 143 | def draw( self, context ): 144 | layout = self.layout 145 | 146 | if context.object is None or context.mode == 'OBJECT': 147 | return layout 148 | 149 | if context.object.type != 'MESH': 150 | return layout 151 | 152 | sel_mode = context.tool_settings.mesh_select_mode[:] 153 | 154 | if sel_mode[0]: 155 | op_vhg = layout.operator( MESH_OT_knifescreen.bl_idname, text='Vertex :: Grid :: Horizontal' ) 156 | op_vhg.str_dir = 'horizontal' 157 | op_vhg.alignment = context.scene.rmkit_props.knifescreenprops.ks_alignment_grid 158 | 159 | op_vht = layout.operator( MESH_OT_knifescreen.bl_idname, text='Vertex :: Screen :: Horizontal' ) 160 | op_vht.str_dir = 'horizontal' 161 | op_vht.alignment = context.scene.rmkit_props.knifescreenprops.ks_alignment_screen 162 | 163 | layout.separator() 164 | 165 | op_vvg = layout.operator( MESH_OT_knifescreen.bl_idname, text='Vertex :: Grid :: Vertical' ) 166 | op_vvg.str_dir = 'vertical' 167 | op_vvg.alignment = context.scene.rmkit_props.knifescreenprops.ks_alignment_grid 168 | 169 | op_vvt = layout.operator( MESH_OT_knifescreen.bl_idname, text='Vertex :: Screen :: Vertical' ) 170 | op_vvt.str_dir = 'vertical' 171 | op_vvt.alignment = context.scene.rmkit_props.knifescreenprops.ks_alignment_screen 172 | 173 | elif sel_mode[1]: 174 | op_et = layout.operator( MESH_OT_knifescreen.bl_idname, text='Edge :: Topo' ) 175 | op_et.alignment = context.scene.rmkit_props.knifescreenprops.ks_alignment_topo 176 | 177 | op_eg = layout.operator( MESH_OT_knifescreen.bl_idname, text='Edge :: Grid' ) 178 | op_eg.alignment = context.scene.rmkit_props.knifescreenprops.ks_alignment_grid 179 | 180 | op_eg = layout.operator( MESH_OT_knifescreen.bl_idname, text='Edge :: Screen' ) 181 | op_eg.alignment = context.scene.rmkit_props.knifescreenprops.ks_alignment_screen 182 | 183 | 184 | class MESH_OT_knifescreenmenu( bpy.types.Operator ): 185 | """Slice the background face selection based on the current vert/edge selection.""" 186 | bl_idname = 'mesh.rm_knifescreenmenu' 187 | bl_label = 'KnifeScreen' 188 | bl_options = { 'UNDO' } 189 | 190 | @classmethod 191 | def poll( cls, context ): 192 | return ( context.area.type == 'VIEW_3D' and 193 | context.active_object is not None and 194 | context.active_object.type == 'MESH' and 195 | context.object.data.is_editmode ) 196 | 197 | def execute( self, context ): 198 | if context.object is None or context.mode == 'OBJECT': 199 | return { 'CANCELLED' } 200 | 201 | if context.object.type != 'MESH': 202 | return { 'CANCELLED' } 203 | 204 | sel_mode = context.tool_settings.mesh_select_mode[:] 205 | if sel_mode[2]: 206 | self.report( { 'ERROR' }, 'KnifeScreen only works in Vertex and Edge modes.' ) 207 | return { 'CANCELLED' } 208 | 209 | bpy.ops.wm.call_menu( name=VIEW3D_MT_knifescreen.bl_idname ) 210 | return { 'FINISHED' } 211 | 212 | 213 | def register(): 214 | bpy.utils.register_class( MESH_OT_knifescreen ) 215 | bpy.utils.register_class( VIEW3D_MT_knifescreen ) 216 | bpy.utils.register_class( MESH_OT_knifescreenmenu ) 217 | 218 | 219 | def unregister(): 220 | bpy.utils.unregister_class( MESH_OT_knifescreen ) 221 | bpy.utils.unregister_class( VIEW3D_MT_knifescreen ) 222 | bpy.utils.unregister_class( MESH_OT_knifescreenmenu ) -------------------------------------------------------------------------------- /loopring.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh, mathutils 2 | import rmlib 3 | 4 | def edge_ring( edge, poly, ring ): 5 | skip_verts = edge.verts 6 | for p in edge.link_faces: 7 | if p.tag or len( p.verts ) != 4: 8 | continue 9 | p.tag = True 10 | 11 | for e in p.edges: 12 | if e.verts[0] in skip_verts or e.verts[1] in skip_verts: 13 | continue 14 | ring.append( e ) 15 | edge_ring( e, p, ring ) 16 | return ring 17 | 18 | return ring 19 | 20 | 21 | def edge_loops( edge, force_boundary=False ): 22 | next_edges = [] 23 | skip_verts = rmlib.rmPolygonSet( edge.link_faces ).vertices 24 | for vert in edge.verts: 25 | if edge.is_boundary: 26 | if not force_boundary and len( vert.link_edges ) != 3: 27 | continue 28 | elif len( edge.link_faces ) == 2 and len( vert.link_edges ) != 4: 29 | continue 30 | 31 | for e in vert.link_edges: 32 | if e == edge or e.tag: 33 | continue 34 | if force_boundary and edge.is_boundary: 35 | if e.is_boundary: 36 | next_edges.append( e ) 37 | e.tag = True 38 | break 39 | else: 40 | if e.other_vert( vert ) not in skip_verts: 41 | next_edges.append( e ) 42 | e.tag = True 43 | break 44 | 45 | return next_edges 46 | 47 | 48 | def edge_loop_alt( edge, vert, loop ): 49 | link_edges = list( vert.link_edges ) 50 | if len( link_edges ) == 1: 51 | return loop 52 | 53 | for e in link_edges: 54 | if e.is_boundary: 55 | e.tag = False 56 | if e in loop: 57 | return loop 58 | else: 59 | next_vert = e.other_vert( vert ) 60 | loop.append( e ) 61 | edge_loop_alt( e, next_vert, loop ) 62 | return loop 63 | 64 | try: 65 | idx = link_edges.index( edge ) 66 | except ValueError: 67 | return loop 68 | link_edges = link_edges[idx+1:] + link_edges[:idx] 69 | next_edge = link_edges[ int( len( link_edges ) / 2 ) ] 70 | next_edge.tag = False 71 | loop.append( next_edge ) 72 | if next_edge in loop: 73 | return loop 74 | 75 | next_vert = next_edge.other_vert( vert ) 76 | edge_loop_alt( next_edge, next_vert, loop ) 77 | return loop 78 | 79 | 80 | class MESH_OT_ring( bpy.types.Operator ): 81 | """Extend current element selection by ring.""" 82 | bl_idname = 'mesh.rm_ring' 83 | bl_label = 'Ring Select' 84 | bl_options = { 'UNDO' } 85 | 86 | unsel: bpy.props.BoolProperty( 87 | name='Deselect', 88 | description='Deselect loop edges.', 89 | default=False 90 | ) 91 | 92 | @classmethod 93 | def poll( cls, context ): 94 | return ( len( context.editable_objects ) > 0 and context.mode == 'EDIT_MESH' ) 95 | 96 | def execute( self, context ): 97 | sel_mode = context.tool_settings.mesh_select_mode[:] 98 | 99 | emptyselection = True 100 | 101 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 102 | with rmmesh as rmmesh: 103 | rmmesh.readonly = True 104 | for e in rmmesh.bmesh.edges: 105 | e.tag = False 106 | for p in rmmesh.bmesh.faces: 107 | p.tag = False 108 | 109 | if sel_mode[0] or sel_mode[1]: 110 | if self.unsel: 111 | selected_edges = rmlib.rmEdgeSet( [rmmesh.bmesh.select_history.active] ) 112 | else: 113 | selected_edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 114 | 115 | elif sel_mode[2]: 116 | selected_polygons = rmlib.rmPolygonSet.from_selection( rmmesh ) 117 | shared_edges = set() 118 | 119 | allEdges = [] 120 | for p in selected_polygons: 121 | allEdges += p.edges 122 | 123 | while( len( allEdges ) > 0 ): 124 | e = allEdges.pop( 0 ) 125 | if e in allEdges: 126 | shared_edges.add( e ) 127 | 128 | selected_edges = rmlib.rmEdgeSet( shared_edges ) 129 | else: 130 | return { 'CANCELLED' } 131 | 132 | if len( selected_edges ) > 0: 133 | emptyselection = False 134 | 135 | for e in selected_edges: 136 | if e.tag: 137 | continue 138 | 139 | ring = rmlib.rmEdgeSet( [e] ) 140 | try: 141 | ring = edge_ring( e, e.link_faces[0], ring ) 142 | ring += edge_ring( e, e.link_faces[1], ring ) 143 | except IndexError: 144 | pass 145 | 146 | #set selection state 147 | if sel_mode[1]: 148 | for e in ring: 149 | e.select = not self.unsel 150 | else: 151 | ring.polygons.select( replace=False ) 152 | 153 | for e in rmmesh.bmesh.edges: 154 | e.tag = False 155 | for p in rmmesh.bmesh.faces: 156 | p.tag = False 157 | 158 | if emptyselection: 159 | self.report( { 'ERROR' }, 'Selection is empty!' ) 160 | return { 'CANCELLED' } 161 | 162 | return { 'FINISHED' } 163 | 164 | 165 | class MESH_OT_loop( bpy.types.Operator ): 166 | """Extend current element selection by loop. Utilizes 3DSMax edge loop algorithm.""" 167 | bl_idname = 'mesh.rm_loop' 168 | bl_label = 'Loop Select' 169 | bl_options = { 'UNDO' } 170 | 171 | force_boundary: bpy.props.BoolProperty( 172 | name='Force Boundary', 173 | description='When True, all loop edges extend along bounary edges.', 174 | default=False 175 | ) 176 | 177 | mode: bpy.props.EnumProperty( 178 | name='Mode', 179 | description='Set/Add/Remove to/from selection.', 180 | items=[ ( "set", "Set", "", 1 ), 181 | ( "add", "Add", "", 2 ), 182 | ( "remove", "Remove", "", 3 ) ], 183 | default='set' 184 | ) 185 | 186 | evaluate_all_selected: bpy.props.BoolProperty( 187 | name='Evaluate All Selected', 188 | description='When True, all selected edges are loop extended.', 189 | default=False, 190 | options={ 'HIDDEN' } 191 | ) 192 | 193 | @classmethod 194 | def poll( cls, context ): 195 | return ( len( context.editable_objects ) > 0 and context.mode == 'EDIT_MESH' ) 196 | 197 | def execute( self, context ): 198 | sel_mode = context.tool_settings.mesh_select_mode[:] 199 | if not sel_mode[1]: 200 | try: 201 | bpy.ops.mesh.rm_ring() 202 | except Exception as e: 203 | self.report( { 'ERROR' }, str( e ) ) 204 | return { 'CANCELLED' } 205 | return { 'FINISHED' } 206 | 207 | emptyselection = True 208 | 209 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 210 | with rmmesh as rmmesh: 211 | for e in rmmesh.bmesh.edges: 212 | e.tag = False 213 | 214 | rmmesh.readonly = True 215 | 216 | if self.mode != 'set' and rmmesh.bmesh.select_history.active is not None and isinstance( rmmesh.bmesh.select_history.active, bmesh.types.BMEdge ) and not self.evaluate_all_selected: 217 | selected_edges = rmlib.rmEdgeSet( [rmmesh.bmesh.select_history.active] ) 218 | else: 219 | selected_edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 220 | 221 | if len( selected_edges ) < 1: 222 | continue 223 | emptyselection = False 224 | 225 | #selected_edges.tag( True ) 226 | while( len( selected_edges ) > 0 ): 227 | e = selected_edges.pop() 228 | for e in edge_loops( e, self.force_boundary ): 229 | e.select = self.mode != 'remove' 230 | selected_edges.append( e ) 231 | 232 | for e in rmmesh.bmesh.edges: 233 | e.tag = False 234 | 235 | if emptyselection: 236 | self.report( { 'ERROR' }, 'Selection is empty!' ) 237 | return { 'CANCELLED' } 238 | 239 | return { 'FINISHED' } 240 | 241 | 242 | def register(): 243 | bpy.utils.register_class( MESH_OT_loop ) 244 | bpy.utils.register_class( MESH_OT_ring ) 245 | 246 | 247 | def unregister(): 248 | bpy.utils.unregister_class( MESH_OT_loop ) 249 | bpy.utils.unregister_class( MESH_OT_ring ) -------------------------------------------------------------------------------- /move_to_furthest.py: -------------------------------------------------------------------------------- 1 | import mathutils 2 | import rmlib 3 | import bpy, bmesh 4 | 5 | def find_furthest( elems, dir_vec, center ): 6 | plane_pos = None 7 | avg_pos = mathutils.Vector() 8 | vcount = 0 9 | for rmmesh, groups in elems.items(): 10 | xfrm = rmmesh.world_transform 11 | for g in groups: 12 | #find the plane_position (min_pos) 13 | avg_pos += xfrm @ g[0].co 14 | vcount += len( g ) 15 | group_plane_pos = xfrm @ g[0].co 16 | max_dot = dir_vec.dot( group_plane_pos ) 17 | for i in range( 1, len( g ) ): 18 | vpos = xfrm @ g[i].co 19 | avg_pos += vpos 20 | dot = dir_vec.dot( vpos ) 21 | if dot > max_dot: 22 | max_dot = dot 23 | group_plane_pos = vpos 24 | if plane_pos is None: 25 | plane_pos = group_plane_pos.copy() 26 | else: 27 | if dir_vec.dot( group_plane_pos ) > dir_vec.dot( plane_pos ): 28 | plane_pos = group_plane_pos 29 | 30 | 31 | #for horizontal/vertical, plane_pos is the avg pos 32 | if center: 33 | avg_pos *= 1.0 / float( vcount ) 34 | plane_pos = avg_pos 35 | 36 | return plane_pos 37 | 38 | 39 | def move_to_furthest( elems, plane_pos, plane_nml, constrain, center, local ): 40 | for rmmesh, groups in elems.items(): 41 | for g in groups: 42 | 43 | if local: 44 | local_elems = { rmmesh : [g] } 45 | plane_pos = find_furthest( local_elems, -plane_nml, center ) 46 | 47 | inv_rot_mat = rmmesh.world_transform.to_3x3().inverted() 48 | plane_nml_objspc = ( inv_rot_mat @ -plane_nml ).normalized() 49 | 50 | inv_xfrm = rmmesh.world_transform.inverted() 51 | plane_pos_objspc = inv_xfrm @ plane_pos 52 | 53 | if constrain: 54 | new_pos = [ None ] * len( g ) 55 | for i, v in enumerate( g ): 56 | max_dot = -1 57 | edge_dir = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 58 | for e in v.link_edges: 59 | other_v = e.other_vert( v ) 60 | edge_vec = other_v.co - v.co 61 | dot = plane_nml_objspc.dot( edge_vec.normalized() ) 62 | if dot >= max_dot: 63 | max_dot = dot 64 | edge_dir = edge_vec 65 | 66 | if max_dot <= 0.00001: 67 | new_pos[i] = v.co 68 | continue 69 | 70 | intersection_pos = mathutils.geometry.intersect_line_plane( v.co, v.co + edge_dir, plane_pos_objspc, plane_nml_objspc ) 71 | if intersection_pos is None: 72 | new_pos[i] = v.co + edge_dir 73 | elif ( v.co - intersection_pos ).length > edge_dir.length: 74 | new_pos[i] = v.co + edge_dir 75 | else: 76 | new_pos[i] = intersection_pos 77 | 78 | for i, v in enumerate( g ): 79 | v.co = new_pos[i] 80 | 81 | else: 82 | for v in g: 83 | dist = mathutils.geometry.distance_point_to_plane( v.co.copy(), plane_pos_objspc, plane_nml_objspc ) 84 | v.co = v.co.copy() + ( -plane_nml_objspc * dist ) 85 | 86 | bmesh.update_edit_mesh( rmmesh.mesh, loop_triangles=True, destructive=True ) 87 | 88 | 89 | class MESH_OT_movetofurthest( bpy.types.Operator ): 90 | """Align selection to a grid axis most aligned with a direction relative to viewport camera.""" 91 | bl_idname = 'mesh.rm_movetofurthest' 92 | bl_label = 'Move To Furthest' 93 | #bl_options = { 'REGISTER', 'UNDO' } 94 | bl_options = { 'UNDO' } 95 | 96 | str_dir: bpy.props.EnumProperty( 97 | items=[ ( "up", "Up", "", 1 ), 98 | ( "down", "Down", "", 2 ), 99 | ( "left", "Left", "", 3 ), 100 | ( "right", "Right", "", 4 ), 101 | ( "horizontal", "Horizontal", "", 5 ), 102 | ( "vertical", "Vertical", "", 6 ) ], 103 | name="Direction", 104 | default="right" 105 | ) 106 | 107 | local: bpy.props.BoolProperty( 108 | name='Local', 109 | description='Group selection based on 3d continuity and align each respectively.', 110 | default=False 111 | ) 112 | 113 | constrain: bpy.props.BoolProperty( 114 | name='Constrain', 115 | description='Constrain all vert translation along linked edges.', 116 | default=False 117 | ) 118 | 119 | @classmethod 120 | def poll( cls, context ): 121 | return ( context.area.type == 'VIEW_3D' and 122 | context.active_object is not None and 123 | context.active_object.type == 'MESH' and 124 | context.object.data.is_editmode ) 125 | 126 | def execute( self, context ): 127 | if context.object is None or context.mode == 'OBJECT': 128 | return { 'CANCELLED' } 129 | 130 | co = context.scene.transform_orientation_slots[0].custom_orientation 131 | grid_matrix = mathutils.Matrix.Identity( 3 ) 132 | if co is not None: 133 | grid_matrix = mathutils.Matrix( co.matrix ).to_3x3() 134 | 135 | rm_vp = rmlib.rmViewport( context ) 136 | dir_idx, cam_dir_vec, grid_dir_vec = rm_vp.get_nearest_direction_vector( self.str_dir, grid_matrix ) 137 | 138 | sel_mode = context.tool_settings.mesh_select_mode[:] 139 | elems = {} 140 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 141 | elems[ rmmesh ] = None 142 | 143 | bm = bmesh.from_edit_mesh( rmmesh.mesh ) 144 | rmmesh.bmesh = bm 145 | 146 | if sel_mode[0]: 147 | selected_verts = rmlib.rmVertexSet.from_selection( rmmesh ) 148 | if len( selected_verts ) < 1: 149 | return { 'CANCELLED' } 150 | if self.local: 151 | vert_groups = selected_verts.group() 152 | else: 153 | vert_groups = [ selected_verts ] 154 | elems[ rmmesh ] = vert_groups 155 | elif sel_mode[1]: 156 | selected_edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 157 | if len( selected_edges ) < 1: 158 | return { 'CANCELLED' } 159 | if self.local: 160 | vert_groups = [ g.vertices for g in selected_edges.group() ] 161 | else: 162 | vert_groups = [ selected_edges.vertices ] 163 | elems[ rmmesh ] = vert_groups 164 | elif sel_mode[2]: 165 | selected_polys = rmlib.rmPolygonSet.from_selection( rmmesh ) 166 | if len( selected_polys ) < 1: 167 | return { 'CANCELLED' } 168 | if self.local: 169 | vert_groups = [ g.vertices for g in selected_polys.group() ] 170 | else: 171 | vert_groups = [ selected_polys.vertices ] 172 | elems[ rmmesh ] = vert_groups 173 | else: 174 | return { 'CANCELLED' } 175 | 176 | center = self.str_dir == 'horizontal' or self.str_dir == 'vertical' 177 | plane_pos = mathutils.Vector() 178 | if not self.local: 179 | plane_pos = find_furthest( elems, grid_dir_vec, center ) 180 | move_to_furthest( elems, plane_pos, -grid_dir_vec, self.constrain, center, self.local ) 181 | 182 | return { 'FINISHED' } 183 | 184 | 185 | class VIEW3D_MT_PIE_movetofurthest( bpy.types.Menu ): 186 | """Align selection to a grid axis most aligned with a direction relative to viewport camera.""" 187 | bl_idname = 'VIEW3D_MT_PIE_movetofurthest' 188 | bl_label = 'Move To Furthest' 189 | 190 | def draw( self, context ): 191 | layout = self.layout 192 | 193 | pie = layout.menu_pie() 194 | 195 | op_l = pie.operator( 'mesh.rm_movetofurthest', text='Left' ) 196 | op_l.str_dir = 'left' 197 | op_l.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 198 | op_l.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 199 | 200 | op_r = pie.operator( 'mesh.rm_movetofurthest', text='Right' ) 201 | op_r.str_dir = 'right' 202 | op_r.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 203 | op_r.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 204 | 205 | op_d = pie.operator( 'mesh.rm_movetofurthest', text='Down' ) 206 | op_d.str_dir = 'down' 207 | op_d.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 208 | op_d.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 209 | 210 | op_u = pie.operator( 'mesh.rm_movetofurthest', text='Up' ) 211 | op_u.str_dir = 'up' 212 | op_u.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 213 | op_u.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 214 | 215 | pie.operator( 'wm.call_menu_pie', text='Con' ).name = 'VIEW3D_MT_PIE_movetofurthest_constrain' 216 | 217 | pie.operator( 'wm.call_menu_pie', text='Local' ).name = 'VIEW3D_MT_PIE_movetofurthest_local' 218 | 219 | op_h = pie.operator( 'mesh.rm_movetofurthest', text='Horizontal' ) 220 | op_h.str_dir = 'vertical' 221 | op_h.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 222 | op_h.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 223 | 224 | op_v = pie.operator( 'mesh.rm_movetofurthest', text='Vertical' ) 225 | op_v.str_dir = 'horizontal' 226 | op_v.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 227 | op_v.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 228 | 229 | 230 | class VIEW3D_MT_PIE_movetofurthest_local( bpy.types.Menu ): 231 | """Align selection to a grid axis most aligned with a direction relative to viewport camera.""" 232 | bl_idname = 'VIEW3D_MT_PIE_movetofurthest_local' 233 | bl_label = 'Move To Furthest LOCAL' 234 | 235 | def draw( self, context ): 236 | layout = self.layout 237 | 238 | pie = layout.menu_pie() 239 | op_l = pie.operator( 'mesh.rm_movetofurthest', text='Left' ) 240 | op_l.str_dir = 'left' 241 | op_l.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 242 | op_l.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 243 | 244 | op_r = pie.operator( 'mesh.rm_movetofurthest', text='Right' ) 245 | op_r.str_dir = 'right' 246 | op_r.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 247 | op_r.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 248 | 249 | op_d = pie.operator( 'mesh.rm_movetofurthest', text='Down' ) 250 | op_d.str_dir = 'down' 251 | op_d.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 252 | op_d.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 253 | 254 | op_u = pie.operator( 'mesh.rm_movetofurthest', text='Up' ) 255 | op_u.str_dir = 'up' 256 | op_u.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 257 | op_u.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 258 | 259 | pie.operator( 'wm.call_menu_pie', text='Constrain' ).name = 'VIEW3D_MT_PIE_movetofurthest_both' 260 | 261 | pie.separator() 262 | 263 | op_h = pie.operator( 'mesh.rm_movetofurthest', text='Horizontal' ) 264 | op_h.str_dir = 'vertical' 265 | op_h.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 266 | op_h.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 267 | 268 | op_v = pie.operator( 'mesh.rm_movetofurthest', text='Vertical' ) 269 | op_v.str_dir = 'horizontal' 270 | op_v.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 271 | op_v.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 272 | 273 | 274 | class VIEW3D_MT_PIE_movetofurthest_constrain( bpy.types.Menu ): 275 | """Align selection to a grid axis most aligned with a direction relative to viewport camera.""" 276 | bl_idname = 'VIEW3D_MT_PIE_movetofurthest_constrain' 277 | bl_label = 'Move To Furthest LOCAL' 278 | 279 | def draw( self, context ): 280 | layout = self.layout 281 | 282 | pie = layout.menu_pie() 283 | op_l = pie.operator( 'mesh.rm_movetofurthest', text='Left' ) 284 | op_l.str_dir = 'left' 285 | op_l.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 286 | op_l.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 287 | 288 | op_r = pie.operator( 'mesh.rm_movetofurthest', text='Right' ) 289 | op_r.str_dir = 'right' 290 | op_r.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 291 | op_r.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 292 | 293 | op_d = pie.operator( 'mesh.rm_movetofurthest', text='Down' ) 294 | op_d.str_dir = 'down' 295 | op_d.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 296 | op_d.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 297 | 298 | op_u = pie.operator( 'mesh.rm_movetofurthest', text='Up' ) 299 | op_u.str_dir = 'up' 300 | op_u.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 301 | op_u.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 302 | 303 | pie.separator() 304 | 305 | pie.operator( 'wm.call_menu_pie', text='Local' ).name = 'VIEW3D_MT_PIE_movetofurthest_both' 306 | 307 | op_h = pie.operator( 'mesh.rm_movetofurthest', text='Horizontal' ) 308 | op_h.str_dir = 'vertical' 309 | op_h.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 310 | op_h.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 311 | 312 | op_v = pie.operator( 'mesh.rm_movetofurthest', text='Vertical' ) 313 | op_v.str_dir = 'horizontal' 314 | op_v.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_off 315 | op_v.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 316 | 317 | 318 | class VIEW3D_MT_PIE_movetofurthest_both( bpy.types.Menu ): 319 | """Align selection to a grid axis most aligned with a direction relative to viewport camera.""" 320 | bl_idname = 'VIEW3D_MT_PIE_movetofurthest_both' 321 | bl_label = 'Move To Furthest LOCAL' 322 | 323 | def draw( self, context ): 324 | layout = self.layout 325 | 326 | pie = layout.menu_pie() 327 | op_l = pie.operator( 'mesh.rm_movetofurthest', text='Left' ) 328 | op_l.str_dir = 'left' 329 | op_l.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 330 | op_l.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 331 | 332 | op_r = pie.operator( 'mesh.rm_movetofurthest', text='Right' ) 333 | op_r.str_dir = 'right' 334 | op_r.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 335 | op_r.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 336 | 337 | op_d = pie.operator( 'mesh.rm_movetofurthest', text='Down' ) 338 | op_d.str_dir = 'down' 339 | op_d.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 340 | op_d.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 341 | 342 | op_u = pie.operator( 'mesh.rm_movetofurthest', text='Up' ) 343 | op_u.str_dir = 'up' 344 | op_u.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 345 | op_u.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 346 | 347 | pie.separator() 348 | 349 | pie.separator() 350 | 351 | op_h = pie.operator( 'mesh.rm_movetofurthest', text='Horizontal' ) 352 | op_h.str_dir = 'vertical' 353 | op_h.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 354 | op_h.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 355 | 356 | op_v = pie.operator( 'mesh.rm_movetofurthest', text='Vertical' ) 357 | op_v.str_dir = 'horizontal' 358 | op_v.local = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 359 | op_v.constrain = context.scene.rmkit_props.movetofurthestprops.mtf_prop_on 360 | 361 | 362 | def register(): 363 | bpy.utils.register_class( MESH_OT_movetofurthest ) 364 | bpy.utils.register_class( VIEW3D_MT_PIE_movetofurthest ) 365 | bpy.utils.register_class( VIEW3D_MT_PIE_movetofurthest_local ) 366 | bpy.utils.register_class( VIEW3D_MT_PIE_movetofurthest_constrain ) 367 | bpy.utils.register_class( VIEW3D_MT_PIE_movetofurthest_both ) 368 | 369 | 370 | def unregister(): 371 | bpy.utils.unregister_class( MESH_OT_movetofurthest ) 372 | bpy.utils.unregister_class( VIEW3D_MT_PIE_movetofurthest ) 373 | bpy.utils.unregister_class( VIEW3D_MT_PIE_movetofurthest_local ) 374 | bpy.utils.unregister_class( VIEW3D_MT_PIE_movetofurthest_constrain ) 375 | bpy.utils.unregister_class( VIEW3D_MT_PIE_movetofurthest_both ) 376 | -------------------------------------------------------------------------------- /naming.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh, mathutils 2 | import rmlib 3 | 4 | 5 | class MESH_OT_itemnametomeshname( bpy.types.Operator ): 6 | """Name all meshes the same as their parent object.""" 7 | bl_idname = 'mesh.rm_itemnametomeshname' 8 | bl_label = 'Item Name to Mesh Name' 9 | bl_options = { 'UNDO' } 10 | 11 | @classmethod 12 | def poll( cls, context ): 13 | return context.area.type == 'VIEW_3D' 14 | 15 | def execute( self, context ): 16 | for obj in bpy.context.selected_objects: 17 | try: 18 | obj.data.name = obj.name 19 | except: 20 | continue 21 | 22 | return { 'FINISHED' } 23 | 24 | 25 | def register(): 26 | bpy.utils.register_class( MESH_OT_itemnametomeshname ) 27 | 28 | 29 | def unregister(): 30 | bpy.utils.unregister_class( MESH_OT_itemnametomeshname ) -------------------------------------------------------------------------------- /panel.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import rmlib 3 | 4 | 5 | class VIEW3D_PT_UTILS( bpy.types.Panel ): 6 | bl_parent_id = 'VIEW3D_PT_RMKIT_PARENT' 7 | bl_label = 'Utilities' 8 | bl_region_type = 'UI' 9 | bl_space_type = 'VIEW_3D' 10 | bl_options = {'DEFAULT_CLOSED'} 11 | 12 | def draw( self, context ): 13 | layout = self.layout 14 | 15 | r1 = layout.row() 16 | r1.alignment = 'EXPAND' 17 | r1.operator( 'mesh.rm_copy', text='Copy' ).cut = False 18 | r1.operator( 'mesh.rm_copy', text='Cut' ).cut = True 19 | r1.operator( 'mesh.rm_paste', text='Paste' ) 20 | 21 | r2 = layout.row() 22 | r2.alignment = 'EXPAND' 23 | r2.operator( 'view3d.rm_togglegrid', text='Grid Toggle' ) 24 | r2.operator( 'view3d.rm_workplane', text='Toggle Workplane' ) 25 | 26 | layout.operator( 'wm.call_menu_pie', text='3D Cursor Pie' ).name = 'VIEW3D_MT_PIE_cursor' 27 | 28 | layout.separator() 29 | 30 | layout.operator( 'view3d.rm_dimensions', text='Toggle Dimensions' ) 31 | layout.prop( context.scene.rmkit_props, 'dimensions_use_background_face_selection' ) 32 | 33 | layout.separator() 34 | 35 | layout.operator( 'mesh.rm_itemnametomeshname' ) 36 | 37 | layout.operator( 'mesh.rm_matclearnup', text='Material Cleanup' ) 38 | 39 | 40 | class VIEW3D_PT_SELECTION( bpy.types.Panel ): 41 | bl_parent_id = 'VIEW3D_PT_RMKIT_PARENT' 42 | bl_label = 'Selection' 43 | bl_region_type = 'UI' 44 | bl_space_type = 'VIEW_3D' 45 | bl_options = {'DEFAULT_CLOSED'} 46 | 47 | def draw( self, context ): 48 | layout = self.layout 49 | 50 | r4 = layout.row() 51 | r4.alignment = 'EXPAND' 52 | 53 | r4.operator( 'mesh.rm_loop', text='Loop' ).force_boundary = False 54 | r4.operator( 'mesh.rm_loop', text='Loop Alt' ).force_boundary = True 55 | 56 | layout.operator( 'mesh.rm_ring', text='Ring' ) 57 | 58 | layout.operator( 'mesh.rm_continuous', text='Set Continuous' ).mode = 'set' 59 | layout.operator( 'mesh.rm_invertcontinuous', text='Invert Continuous' ) 60 | 61 | 62 | class VIEW3D_PT_MESHEDIT( bpy.types.Panel ): 63 | bl_parent_id = 'VIEW3D_PT_RMKIT_PARENT' 64 | bl_label = 'Mesh Edit' 65 | bl_region_type = 'UI' 66 | bl_space_type = 'VIEW_3D' 67 | bl_options = {'DEFAULT_CLOSED'} 68 | 69 | def draw( self, context ): 70 | layout = self.layout 71 | 72 | layout.operator( 'mesh.rm_polypatch', text='PolyPatch' ) 73 | 74 | box1 = layout.box() 75 | r2 = box1.row() 76 | r2.alignment = 'EXPAND' 77 | r2.operator( 'mesh.rm_remove', text='Delete' ).reduce_mode = 'DEL' 78 | r2.operator( 'mesh.rm_remove', text='Collapse' ).reduce_mode = 'COL' 79 | r2.operator( 'mesh.rm_remove', text='Dissolve' ).reduce_mode = 'DIS' 80 | r2.operator( 'mesh.rm_remove', text='Pop' ).reduce_mode = 'POP' 81 | 82 | c1 = layout.column() 83 | c1.operator( 'mesh.rm_knifescreenmenu', text='Knife Screen' ) 84 | c1.operator( 'wm.call_menu_pie', text='Move To Furthest' ).name = 'VIEW3D_MT_PIE_movetofurthest' 85 | c1.operator( 'wm.call_menu_pie', text='Screen Reflect' ).name = 'OBJECT_MT_rm_screenreflect' 86 | c1.operator( 'mesh.rm_falloff', text='Falloff Transform' ) 87 | 88 | r2 = layout.row() 89 | r2.operator( 'mesh.rm_contextbevel', text='Bevel' ) 90 | r2.operator( 'mesh.rm_extend', text='Extend' ) 91 | 92 | c2 = layout.column() 93 | c2.operator( 'mesh.rm_connectedge', text='Connect Edges' ) 94 | c2.operator( 'mesh.rm_thicken', text='Thicken' ) 95 | c2.operator( 'mesh.rm_createtube', text='Create Tube' ) 96 | c2.operator( 'mesh.rm_arcadjust', text='Arc Adjust' ) 97 | c2.operator( 'mesh.rm_extrudealongpath', text='Extrude Along Path' ) 98 | 99 | c3 = layout.column() 100 | c3.operator( 'mesh.rm_unbevel', text='Unbevel' ) 101 | c3.operator( 'mesh.rm_radialalign', text='Radial Align' ) 102 | c3.operator( 'mesh.rm_targetweld', text='Target Weld' ) 103 | 104 | 105 | class VIEW3D_PT_LAYERS( bpy.types.Panel ): 106 | bl_idname = 'VIEW3D_PT_LAYERS' 107 | bl_parent_id = 'VIEW3D_PT_RMKIT_PARENT' 108 | bl_label = 'Mesh Layers' 109 | bl_region_type = 'UI' 110 | bl_space_type = 'VIEW_3D' 111 | bl_options = {'DEFAULT_CLOSED'} 112 | 113 | def draw( self, context ): 114 | layout = self.layout 115 | 116 | layout.operator( 'wm.call_menu_pie', text='Edge Weight Pie' ).name = 'VIEW3D_MT_PIE_setedgeweight_crease' 117 | 118 | 119 | def register(): 120 | bpy.utils.register_class( VIEW3D_PT_UTILS ) 121 | bpy.utils.register_class( VIEW3D_PT_SELECTION ) 122 | bpy.utils.register_class( VIEW3D_PT_MESHEDIT ) 123 | bpy.utils.register_class( VIEW3D_PT_LAYERS ) 124 | 125 | 126 | def unregister(): 127 | bpy.utils.unregister_class( VIEW3D_PT_UTILS ) 128 | bpy.utils.unregister_class( VIEW3D_PT_SELECTION ) 129 | bpy.utils.unregister_class( VIEW3D_PT_MESHEDIT ) 130 | bpy.utils.unregister_class( VIEW3D_PT_LAYERS ) -------------------------------------------------------------------------------- /polypatch.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import rmlib 4 | 5 | def select_vchain( vchain, replace=False ): 6 | if replace: 7 | bpy.ops.mesh.select_all( action = 'DESELECT' ) 8 | for vp in vchain: 9 | be = rmlib.rmEdgeSet.from_endpoints( vp[0], vp[1] ) 10 | if be is None: 11 | continue 12 | be.select = True 13 | 14 | class MESH_OT_polypatch( bpy.types.Operator ): 15 | """Mesh editing operator that modifies topology based off selection and context.""" 16 | bl_idname = 'mesh.rm_polypatch' 17 | bl_label = 'PolyPatch' 18 | bl_options = { 'UNDO' } 19 | 20 | @classmethod 21 | def poll( cls, context ): 22 | return ( context.area.type == 'VIEW_3D' and 23 | context.active_object is not None and 24 | context.active_object.type == 'MESH' and 25 | context.active_object.data.is_editmode ) 26 | 27 | def execute( self, context ): 28 | sel_mode = context.tool_settings.mesh_select_mode[:] 29 | rmmesh = rmlib.rmMesh.GetActive( context ) 30 | if rmmesh is None: 31 | self.report( { 'ERROR' }, 'Could not get active mesh.' ) 32 | return { 'CANCELLED' } 33 | 34 | if sel_mode[0]: 35 | with rmmesh as rmmesh: 36 | sel_verts = rmlib.rmVertexSet.from_selection( rmmesh ) 37 | if len( sel_verts ) < 2: 38 | self.report( { 'INFO' }, 'Must have two or more verts selected!!!' ) 39 | return { 'CANCELLED' } 40 | slice_edges = bmesh.ops.connect_verts( rmmesh.bmesh, verts=sel_verts, check_degenerate=False ) 41 | 42 | elif sel_mode[1]: 43 | with rmmesh as rmmesh: 44 | rmmesh.readonly = True 45 | sel_edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 46 | open_edges = rmlib.rmEdgeSet() 47 | closed_edges = rmlib.rmEdgeSet() 48 | for e in sel_edges: 49 | if e.is_boundary: 50 | open_edges.append( e ) 51 | elif e.is_contiguous: 52 | closed_edges.append( e ) 53 | 54 | somethinghappened = False 55 | 56 | if len( open_edges ) > 0: 57 | vchains = open_edges.chain() 58 | 59 | #break up vchains into edge sets of open and closed loops 60 | closed_loops = [] 61 | open_loops = [] 62 | for vchain in vchains: 63 | elist = rmlib.rmEdgeSet() 64 | for vp in vchain: 65 | elist.append( rmlib.rmEdgeSet.from_endpoints( vp[0], vp[1] ) ) 66 | 67 | if vchain[0][0] == vchain[-1][-1]: 68 | closed_loops.append( elist ) 69 | else: 70 | open_loops.append( elist ) 71 | 72 | #close loops get capped 73 | if len( closed_loops ) > 0: 74 | somethinghappened = True 75 | bpy.ops.mesh.select_all( action = 'DESELECT' ) 76 | for loop in closed_loops: 77 | loop.select( False ) 78 | bpy.ops.mesh.edge_face_add() 79 | 80 | #if there are two open loops, then bridge 81 | if len( open_loops ) == 2: 82 | somethinghappened = True 83 | bpy.ops.mesh.select_all( action = 'DESELECT' ) 84 | for loop in open_loops: 85 | loop.select( False ) 86 | bpy.ops.mesh.bridge_edge_loops() 87 | 88 | #otherwise just face_add 89 | elif len( open_loops ) > 0: 90 | somethinghappened = True 91 | bpy.ops.mesh.select_all( action = 'DESELECT' ) 92 | for loop in open_loops: 93 | loop.select( False ) 94 | bpy.ops.mesh.edge_face_add() 95 | 96 | #closed edges get rotated 97 | if len( closed_edges ) > 0: 98 | somethinghappened = True 99 | closed_edges.select( True ) 100 | bpy.ops.mesh.edge_rotate( use_ccw=True ) 101 | 102 | if not somethinghappened: 103 | self.report( { 'INFO' }, 'No actionable edge selection found.' ) 104 | return { 'CANCELLED' } 105 | 106 | elif sel_mode[2]: 107 | with rmmesh as rmmesh: 108 | rmmesh.readonly = True 109 | sel_polys = rmlib.rmPolygonSet.from_selection( rmmesh ) 110 | groups = sel_polys.group() 111 | if len( groups ) == 2: 112 | bpy.ops.mesh.bridge_edge_loops() 113 | else: 114 | self.report( { 'INFO' }, 'Exactly two face groups must be selected in order to bridge.' ) 115 | return { 'CANCELLED' } 116 | 117 | return { 'FINISHED' } 118 | 119 | def register(): 120 | bpy.utils.register_class( MESH_OT_polypatch ) 121 | 122 | def unregister(): 123 | bpy.utils.unregister_class( MESH_OT_polypatch ) -------------------------------------------------------------------------------- /preferences.py: -------------------------------------------------------------------------------- 1 | import bpy, rna_keymap_ui 2 | 3 | RM_3DVIEW_KEYMAP = [] 4 | RM_MESH_KEYMAP = [] 5 | RM_GUI_NAMES = set() 6 | 7 | #https://docs.blender.org/api/current/bpy.types.KeyMapItems.html#bpy.types.KeyMapItems 8 | 9 | def register_keyboard_keymap(): 10 | kc = bpy.context.window_manager.keyconfigs.addon 11 | if kc: 12 | km_3dview = kc.keymaps.new( name='3D View', space_type='VIEW_3D' ) 13 | km_mesh = kc.keymaps.new( name='Mesh', space_type='EMPTY' ) 14 | 15 | #3D VIEW KEYMAPS 16 | RM_3DVIEW_KEYMAP.append( ( km_3dview, km_3dview.keymap_items.new( 'view3d.rm_dimensions', 'NONE', 'PRESS' ) ) ) 17 | RM_3DVIEW_KEYMAP.append( ( km_3dview, km_3dview.keymap_items.new( 'view3d.rm_togglegrid', 'NONE', 'PRESS' ) ) ) 18 | RM_3DVIEW_KEYMAP.append( ( km_3dview, km_3dview.keymap_items.new( 'view3d.rm_workplane', 'NONE', 'PRESS' ) ) ) 19 | RM_3DVIEW_KEYMAP.append( ( km_3dview, km_3dview.keymap_items.new( 'view3d.rm_cursor_to_selection', 'NONE', 'PRESS' ) ) ) 20 | RM_3DVIEW_KEYMAP.append( ( km_3dview, km_3dview.keymap_items.new( 'view3d.rm_unrotate_relative_to_cursor', 'NONE', 'PRESS' ) ) ) 21 | RM_3DVIEW_KEYMAP.append( ( km_3dview, km_3dview.keymap_items.new( 'object.rm_origin_to_cursor', 'NONE', 'PRESS' ) ) ) 22 | RM_3DVIEW_KEYMAP.append( ( km_3dview, km_3dview.keymap_items.new( 'mesh.rm_matclearnup', 'NONE', 'PRESS' ) ) ) 23 | 24 | kmi = km_3dview.keymap_items.new( 'wm.call_menu_pie', 'NONE', 'PRESS' ) 25 | kmi.properties.name = 'VIEW3D_MT_PIE_cursor' 26 | RM_GUI_NAMES.add( 'VIEW3D_MT_PIE_cursor' ) 27 | RM_3DVIEW_KEYMAP.append( ( km_3dview, kmi ) ) 28 | 29 | kmi = km_3dview.keymap_items.new( 'wm.call_menu_pie', 'NONE', 'PRESS' ) 30 | kmi.properties.name = 'OBJECT_MT_rm_screenreflect' 31 | RM_GUI_NAMES.add( 'OBJECT_MT_rm_screenreflect' ) 32 | RM_3DVIEW_KEYMAP.append( ( km_3dview, kmi ) ) 33 | 34 | kmi = km_3dview.keymap_items.new( 'mesh.rm_changemodeto', 'NONE', 'PRESS' ) 35 | kmi.properties.mode_to = 'FACE' 36 | RM_3DVIEW_KEYMAP.append( ( km_3dview, kmi ) ) 37 | 38 | kmi = km_3dview.keymap_items.new( 'mesh.rm_changemodeto', 'NONE', 'PRESS' ) 39 | kmi.properties.mode_to = 'EDGE' 40 | RM_3DVIEW_KEYMAP.append( ( km_3dview, kmi ) ) 41 | 42 | kmi = km_3dview.keymap_items.new( 'mesh.rm_changemodeto', 'NONE', 'PRESS' ) 43 | kmi.properties.mode_to = 'VERT' 44 | RM_3DVIEW_KEYMAP.append( ( km_3dview, kmi ) ) 45 | 46 | 47 | #MESH KEYMAPS 48 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_unbevel', 'NONE', 'PRESS' ) ) ) 49 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_arcadjust', 'NONE', 'PRESS' ) ) ) 50 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_connectedge', 'NONE', 'PRESS' ) ) ) 51 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_contextbevel', 'NONE', 'PRESS' ) ) ) 52 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_createtube', 'NONE', 'PRESS' ) ) ) 53 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_extend', 'NONE', 'PRESS' ) ) ) 54 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_grabapplymat', 'NONE', 'PRESS' ) ) ) 55 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_grabapplyvcolor', 'NONE', 'PRESS' ) ) ) 56 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_quickmaterial', 'NONE', 'PRESS' ) ) ) 57 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_radialalign', 'NONE', 'PRESS' ) ) ) 58 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_loop', 'NONE', 'PRESS' ) ) ) 59 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_ring', 'NONE', 'PRESS' ) ) ) 60 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_polypatch', 'NONE', 'PRESS' ) ) ) 61 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_invertcontinuous', 'NONE', 'PRESS' ) ) ) 62 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_targetweld', 'NONE', 'PRESS' ) ) ) 63 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_thicken', 'NONE', 'PRESS' ) ) ) 64 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_knifescreenmenu', 'NONE', 'PRESS' ) ) ) 65 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_extrudealongpath', 'NONE', 'PRESS' ) ) ) 66 | 67 | kmi = km_mesh.keymap_items.new( 'mesh.rm_remove', 'NONE', 'PRESS' ) 68 | kmi.properties.reduce_mode = 'DIS' 69 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 70 | 71 | kmi = km_mesh.keymap_items.new( 'mesh.rm_remove', 'NONE', 'PRESS' ) 72 | kmi.properties.reduce_mode = 'COL' 73 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 74 | 75 | kmi = km_mesh.keymap_items.new( 'mesh.rm_remove', 'NONE', 'PRESS' ) 76 | kmi.properties.reduce_mode = 'POP' 77 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 78 | 79 | kmi = km_mesh.keymap_items.new( 'mesh.rm_remove', 'NONE', 'PRESS' ) 80 | kmi.properties.reduce_mode = 'DEL' 81 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 82 | 83 | kmi = km_mesh.keymap_items.new( 'mesh.rm_copy', 'NONE', 'PRESS' ) 84 | kmi.properties.cut = True 85 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 86 | 87 | kmi = km_mesh.keymap_items.new( 'mesh.rm_copy', 'NONE', 'PRESS' ) 88 | kmi.properties.cut = False 89 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 90 | 91 | kmi = km_mesh.keymap_items.new( 'mesh.rm_paste', 'NONE', 'PRESS' ) 92 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 93 | 94 | kmi = km_mesh.keymap_items.new( 'mesh.rm_convertmodeto', 'NONE', 'PRESS' ) 95 | kmi.properties.mode_to = 'FACE' 96 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 97 | 98 | kmi = km_mesh.keymap_items.new( 'mesh.rm_convertmodeto', 'NONE', 'PRESS' ) 99 | kmi.properties.mode_to = 'EDGE' 100 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 101 | 102 | kmi = km_mesh.keymap_items.new( 'mesh.rm_convertmodeto', 'NONE', 'PRESS' ) 103 | kmi.properties.mode_to = 'VERT' 104 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 105 | 106 | kmi = km_mesh.keymap_items.new( 'mesh.rm_continuous', 'NONE', 'PRESS' ) 107 | kmi.properties.mode = 'remove' 108 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 109 | 110 | kmi = km_mesh.keymap_items.new( 'mesh.rm_continuous', 'NONE', 'PRESS' ) 111 | kmi.properties.mode = 'add' 112 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 113 | 114 | kmi = km_mesh.keymap_items.new( 'mesh.rm_continuous', 'NONE', 'PRESS' ) 115 | kmi.properties.mode = 'set' 116 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 117 | 118 | kmi = km_mesh.keymap_items.new( 'wm.call_menu_pie', 'NONE', 'PRESS' ) 119 | kmi.properties.name = 'VIEW3D_MT_PIE_movetofurthest' 120 | RM_GUI_NAMES.add( 'VIEW3D_MT_PIE_movetofurthest' ) 121 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 122 | 123 | kmi = km_mesh.keymap_items.new( 'wm.call_menu_pie', 'NONE', 'PRESS' ) 124 | kmi.properties.name = 'VIEW3D_MT_PIE_setedgeweight_crease' 125 | RM_GUI_NAMES.add( 'VIEW3D_MT_PIE_setedgeweight_crease' ) 126 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 127 | 128 | kmi = km_mesh.keymap_items.new( 'wm.call_menu_pie', 'NONE', 'PRESS' ) 129 | kmi.properties.name = 'VIEW3D_MT_PIE_quicklineardeform' 130 | RM_GUI_NAMES.add( 'VIEW3D_MT_PIE_quicklineardeform' ) 131 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 132 | 133 | RM_MESH_KEYMAP.append( ( km_mesh, km_mesh.keymap_items.new( 'mesh.rm_falloff', 'NONE', 'PRESS' ) ) ) 134 | 135 | kmi = km_mesh.keymap_items.new( 'wm.call_menu', 'NONE', 'PRESS' ) 136 | kmi.properties.name = 'VIEW3D_MT_quickbool' 137 | RM_GUI_NAMES.add( 'VIEW3D_MT_quickbool' ) 138 | RM_MESH_KEYMAP.append( ( km_mesh, kmi ) ) 139 | 140 | def unregister_keyboard_keymap(): 141 | for km, kmi in RM_3DVIEW_KEYMAP: 142 | km.keymap_items.remove( kmi ) 143 | for km, kmi in RM_MESH_KEYMAP: 144 | km.keymap_items.remove( kmi ) 145 | RM_3DVIEW_KEYMAP.clear() 146 | RM_MESH_KEYMAP.clear() 147 | RM_GUI_NAMES.clear() 148 | 149 | 150 | def set_basepath(self, value): 151 | if '$ObjectName' not in value: 152 | return 153 | self["em_basepath"] = value 154 | 155 | 156 | def get_basepath(self): 157 | try: 158 | return self["em_basepath"] 159 | except KeyError: 160 | return '$SceneDir\\$SceneName_$ObjectName' 161 | 162 | 163 | class RMKITPreferences( bpy.types.AddonPreferences ): 164 | bl_idname = __package__ 165 | 166 | export_manager_basepath: bpy.props.StringProperty(name='BasePath', default='$SceneDir\\$SceneName_$ObjectName', get=get_basepath, set=set_basepath) 167 | 168 | v3d_checkbox: bpy.props.BoolProperty( name="3D View", default=False ) 169 | mesh_checkbox: bpy.props.BoolProperty( name="Mesh", default=False ) 170 | 171 | def draw( self, context ): 172 | layout = self.layout 173 | 174 | box = layout.box() 175 | 176 | row_view3d = box.row() 177 | row_view3d.prop( self, 'v3d_checkbox', icon='TRIA_DOWN' if self.v3d_checkbox else 'TRIA_RIGHT', icon_only=True, emboss=False ) 178 | row_view3d.label( text='3D View' ) 179 | if self.v3d_checkbox: 180 | col = box.column( align=True ) 181 | self.draw_keymap_items( col, '3D View', RM_3DVIEW_KEYMAP, {'ACTIONZONE', 'KEYBOARD', 'MOUSE', 'NDOF'}, False ) 182 | 183 | row_mesh = box.row() 184 | row_mesh.prop( self, 'mesh_checkbox', icon='TRIA_DOWN' if self.mesh_checkbox else 'TRIA_RIGHT', icon_only=True, emboss=False ) 185 | row_mesh.label( text='Mesh' ) 186 | if self.mesh_checkbox: 187 | col = box.column( align=True ) 188 | self.draw_keymap_items( col, 'Mesh', RM_MESH_KEYMAP, {'ACTIONZONE', 'KEYBOARD', 'MOUSE', 'NDOF'}, False ) 189 | 190 | 191 | @staticmethod 192 | def draw_keymap_items( col, km_name, keymap, map_type, allow_remove=False ): 193 | kc = bpy.context.window_manager.keyconfigs.user 194 | km = kc.keymaps.get( km_name ) 195 | kmi_idnames = [ km_tuple[1].idname for km_tuple in keymap ] 196 | if allow_remove: 197 | col.context_pointer_set( 'keymap', km ) 198 | 199 | for kmi in km.keymap_items: 200 | if kmi.idname in kmi_idnames and kmi.map_type in map_type: 201 | if kmi.idname == 'wm.call_menu_pie' or kmi.idname == 'wm.call_menu': 202 | if kmi.properties.name not in RM_GUI_NAMES: 203 | continue 204 | rna_keymap_ui.draw_kmi( ['ADDON', 'USER', 'DEFAULT'], kc, km, kmi, col, 0 ) 205 | 206 | 207 | def register(): 208 | bpy.utils.register_class( RMKITPreferences ) 209 | register_keyboard_keymap() 210 | 211 | 212 | def unregister(): 213 | bpy.utils.unregister_class( RMKITPreferences ) 214 | unregister_keyboard_keymap() -------------------------------------------------------------------------------- /propertygroup.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | class WorkplaneGridVisibility( bpy.types.PropertyGroup ): 4 | prop_show_floor: bpy.props.BoolProperty( name="Show Floor", default=True ) 5 | prop_show_x: bpy.props.BoolProperty( name="Show X Axis", default=True ) 6 | prop_show_y: bpy.props.BoolProperty( name="Show Y Axis", default=True ) 7 | prop_show_z: bpy.props.BoolProperty( name="Show Z Axis", default=False ) 8 | 9 | class EdgeWeightProperties( bpy.types.PropertyGroup ): 10 | ew_weight_type_crease: bpy.props.EnumProperty( 11 | items=[ ( "crease", "Crease", "", 1 ), 12 | ( "bevel_weight", "Bevel Weight", "", 2 ), 13 | ( "sharp", "Sharp", "", 3 ) ], 14 | name="Weight Type", 15 | default="crease" 16 | ) 17 | ew_weight_type_bevel_weight: bpy.props.EnumProperty( 18 | items=[ ( "crease", "Crease", "", 1 ), 19 | ( "bevel_weight", "Bevel Weight", "", 2 ), 20 | ( "sharp", "Sharp", "", 3 ) ], 21 | name="Weight Type", 22 | default="bevel_weight" 23 | ) 24 | 25 | class KnifeScreenProperties( bpy.types.PropertyGroup ): 26 | ks_alignment_topo: bpy.props.EnumProperty( 27 | items=[ ( "topology", "Topology", "", 1 ), 28 | ( "grid", "Grid", "", 2 ), 29 | ( "screen", "Screen", "", 3 ) ], 30 | name="Alignment", 31 | default="topology" 32 | ) 33 | ks_alignment_grid: bpy.props.EnumProperty( 34 | items=[ ( "topology", "Topology", "", 1 ), 35 | ( "grid", "Grid", "", 2 ), 36 | ( "screen", "Screen", "", 3 ) ], 37 | name="Alignment", 38 | default="grid" 39 | ) 40 | ks_alignment_screen: bpy.props.EnumProperty( 41 | items=[ ( "topology", "Topology", "", 1 ), 42 | ( "grid", "Grid", "", 2 ), 43 | ( "screen", "Screen", "", 3 ) ], 44 | name="Alignment", 45 | default="screen" 46 | ) 47 | 48 | 49 | class MoveToFurthestProperties( bpy.types.PropertyGroup ): 50 | mtf_prop_on: bpy.props.BoolProperty( default=True ) 51 | mtf_prop_off: bpy.props.BoolProperty( default=False ) 52 | 53 | 54 | class ScreenReflectProperties( bpy.types.PropertyGroup ): 55 | sr_0: bpy.props.IntProperty( default=0 ) 56 | sr_1: bpy.props.IntProperty( default=1 ) 57 | sr_2: bpy.props.IntProperty( default=2 ) 58 | 59 | 60 | class RMKitSceneProperties(bpy.types.PropertyGroup): 61 | # Properties from dimensions.py 62 | dimensions_use_background_face_selection: bpy.props.BoolProperty( 63 | name="Use Background Face Selection", 64 | ) 65 | 66 | # Properties from vnormals.py 67 | vn_selsetweighted: bpy.props.BoolProperty( 68 | name="Area Weights", 69 | default=False, 70 | description="Use triangle surface area weights when computing for vertex normals." 71 | ) 72 | 73 | # Properties from workplane.py 74 | workplaneprops: bpy.props.PointerProperty( type=WorkplaneGridVisibility ) 75 | 76 | # Properties from edgeweight.py 77 | edgeweightprops: bpy.props.PointerProperty( type=EdgeWeightProperties ) 78 | 79 | # Properties from knifescreen.py 80 | knifescreenprops: bpy.props.PointerProperty( type=KnifeScreenProperties ) 81 | 82 | # Properties from movetofurthest.py 83 | movetofurthestprops: bpy.props.PointerProperty( type=MoveToFurthestProperties ) 84 | 85 | # Properties from screenreflect.py 86 | screenreflectprops: bpy.props.PointerProperty( type=ScreenReflectProperties ) 87 | 88 | 89 | def register(): 90 | print( "Registering RMKit Properties..." ) 91 | bpy.utils.register_class(WorkplaneGridVisibility) 92 | bpy.utils.register_class(EdgeWeightProperties) 93 | bpy.utils.register_class(KnifeScreenProperties) 94 | bpy.utils.register_class(MoveToFurthestProperties) 95 | bpy.utils.register_class(ScreenReflectProperties) 96 | bpy.utils.register_class(RMKitSceneProperties) 97 | bpy.types.Scene.rmkit_props = bpy.props.PointerProperty(type=RMKitSceneProperties) 98 | 99 | def unregister(): 100 | bpy.utils.unregister_class(WorkplaneGridVisibility) 101 | bpy.utils.unregister_class(EdgeWeightProperties) 102 | bpy.utils.unregister_class(KnifeScreenProperties) 103 | bpy.utils.unregister_class(MoveToFurthestProperties) 104 | bpy.utils.unregister_class(ScreenReflectProperties) 105 | bpy.utils.unregister_class(RMKitSceneProperties) 106 | del bpy.types.Scene.rmkit_props -------------------------------------------------------------------------------- /quickboolean.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import rmlib 3 | 4 | BOOL_COLLECTION_KEY = 'rcl' 5 | BOOL_COLLECTIONTYPE_KEY = 'rct' 6 | BOOL_PARENT_KEY = 'rbp' 7 | BOOL_CHILD_KEY = 'rbc' 8 | BOOL_DUO_KEY = 'rdo' 9 | 10 | class BoolCollectionManager(): 11 | def __init__( self, scene ): 12 | self.__scene = scene 13 | self.__rootcollection = self.GetRootCollection() 14 | 15 | def GetRootCollection( self ): 16 | for c in self.__scene.collection.children: 17 | if BOOL_COLLECTION_KEY in c.keys() and c[BOOL_COLLECTION_KEY] == 'root': 18 | return c 19 | c = bpy.data.collections.new( 'RM Boolean' ) 20 | c[BOOL_COLLECTION_KEY] = 'root' 21 | self.__scene.collection.children.link( c ) 22 | return c 23 | 24 | def ClearObjectFromCollections( self, obj ): 25 | for c in obj.users_collection: 26 | c.objects.unlink( obj ) 27 | self.__scene.collection.objects.link( obj ) 28 | obj.display_type = 'TEXTURED' 29 | 30 | def AddObjectToCollection( self, obj, parent, type ): 31 | for c in obj.users_collection: 32 | c.objects.unlink( obj ) 33 | for c in parent.users_collection: 34 | c.objects.link( obj ) 35 | obj.parent = parent 36 | obj.display_type = 'WIRE' 37 | for c in self.__rootcollection.children: 38 | try: 39 | if c[BOOL_COLLECTION_KEY] == parent.name_full and c[BOOL_COLLECTIONTYPE_KEY] == type: 40 | c.objects.link( obj ) 41 | obj.display_type = 'WIRE' 42 | return True 43 | except KeyError: 44 | continue 45 | 46 | #fail case 47 | for c in obj.users_collection: 48 | c.objects.unlink( obj ) 49 | obj.display_type = 'TEXTURED' 50 | obj.parent = None 51 | return False 52 | 53 | def CreateCollection( self, obj, parent, type ): 54 | c = bpy.data.collections.new( '{}_{}_{}'.format( BOOL_COLLECTION_KEY, parent.name, type ) ) 55 | c[BOOL_COLLECTION_KEY] = parent.name_full 56 | c[BOOL_COLLECTIONTYPE_KEY] = type 57 | self.__rootcollection.children.link( c ) 58 | 59 | def GetCollection( self, parent, type ): 60 | for c in self.__rootcollection.children: 61 | try: 62 | if c[BOOL_COLLECTION_KEY] == parent.name_full and c[BOOL_COLLECTIONTYPE_KEY] == type: 63 | return c 64 | except KeyError: 65 | continue 66 | return None 67 | 68 | @property 69 | def root_collection( self ): 70 | return self.__rootcollection 71 | 72 | 73 | class DUO_Wrapper(): 74 | def __init__( self, obj ): 75 | self.__duo = obj 76 | 77 | @staticmethod 78 | def CreateDriveUnionObj( parent, type ): 79 | mesh = bpy.data.meshes.new( '{}_mesh'.format( BOOL_DUO_KEY ) ) 80 | duo_obj = bpy.data.objects.new( BOOL_DUO_KEY, mesh ) 81 | #duo_obj.parent = parent 82 | 83 | collections_mod = BoolCollectionManager( parent.users_scene[0] ) 84 | collections_mod.root_collection.objects.link( duo_obj ) 85 | #parent.users_collection[0].objects.link( duo_obj ) 86 | 87 | duo_obj[BOOL_DUO_KEY] = parent.name_full 88 | m = duo_obj.modifiers.new( name='{}_{}'.format( BOOL_DUO_KEY, type ), type='BOOLEAN' ) 89 | m.operation = 'UNION' 90 | m.operand_type = 'COLLECTION' 91 | duo_obj.hide_select = True 92 | duo_obj.hide_viewport = True 93 | duo_obj.hide_render = True 94 | return duo_obj 95 | 96 | @classmethod 97 | def InitDUO( cls, parent, type ): 98 | #find boolean obj if available 99 | for m in parent.modifiers: 100 | if m.type == 'BOOLEAN' and m.operation == type and m.operand_type == 'COLLECTION' and m.object is None: 101 | m.object = DUO_Wrapper.CreateDriveUnionObj( parent, type ) 102 | return cls( m.object ) 103 | 104 | #create drive union object and add to parent 105 | duo = DUO_Wrapper.CreateDriveUnionObj( parent, type ) 106 | m = parent.modifiers.new( name='{}_{}'.format( BOOL_DUO_KEY, type ), type='BOOLEAN' ) 107 | m.object = duo 108 | m.operation = type 109 | 110 | return cls( duo ) 111 | 112 | def SetCollection( self, collection, type ): 113 | for m in self.__duo.modifiers: 114 | if m.type == 'BOOLEAN' and m.name.startswith( '{}_{}'.format( BOOL_DUO_KEY, type ) ) and m.operand_type == 'COLLECTION': 115 | m.collection = collection 116 | return True 117 | return False 118 | 119 | 120 | def MakeParent( obj ): 121 | if obj.type != 'MESH': 122 | raise TypeError 123 | obj[BOOL_PARENT_KEY] = 'parent' 124 | 125 | 126 | def IsParent( obj ): 127 | if not obj.type == 'MESH': 128 | return False 129 | return BOOL_PARENT_KEY in obj.keys() 130 | 131 | 132 | def MakeChild( obj, parent ): 133 | if obj.type != 'MESH': 134 | raise TypeError 135 | obj[BOOL_CHILD_KEY] = parent.name_full 136 | 137 | 138 | def IsChild( obj ): 139 | if not obj.type == 'MESH': 140 | return False 141 | return BOOL_CHILD_KEY in obj.keys() 142 | 143 | 144 | def AddBoolean( obj, parent, type ): 145 | if IsParent( obj ): 146 | return False 147 | 148 | collections_mod = BoolCollectionManager( obj.users_scene[0] ) 149 | 150 | if IsChild( obj ): 151 | collections_mod.ClearObjectFromCollections( obj ) 152 | else: 153 | MakeChild( obj, parent ) 154 | 155 | if not collections_mod.AddObjectToCollection( obj, parent, type ): 156 | collections_mod.CreateCollection( obj, parent, type ) 157 | collections_mod.AddObjectToCollection( obj, parent, type ) 158 | 159 | c = collections_mod.GetCollection( parent, type ) 160 | duo = DUO_Wrapper.InitDUO( parent, type ) 161 | duo.SetCollection( c, type ) 162 | 163 | return True 164 | 165 | 166 | def CleanupQuickBool( context ): 167 | collections_mod = BoolCollectionManager( context.scene ) 168 | #remove empty collections 169 | for c in collections_mod.root_collection.children: 170 | for o in list( c.objects ): 171 | if len( o.data.polygons ) < 1: 172 | bpy.data.objects.remove( o, do_unlink=True ) 173 | if len( c.objects ) == 0: 174 | bpy.data.collections.remove( c ) 175 | 176 | #remove duo objects with missing collections or missing parents 177 | for o in list( collections_mod.root_collection.objects ): 178 | #missing parent 179 | try: 180 | bpy.data.objects[o[BOOL_DUO_KEY]] #get parent by name 181 | except LookupError: 182 | print( 'failed to get parent by name {}'.format( o[BOOL_DUO_KEY] ) ) 183 | bpy.data.objects.remove( o, do_unlink=True ) 184 | continue 185 | 186 | #missing collection 187 | for m in list( o.modifiers ): 188 | if m.type == 'BOOLEAN': 189 | if m.collection is None: 190 | o.modifiers.remove( m ) 191 | 192 | #duo has no mods 193 | if len( o.modifiers ) < 1: 194 | bpy.data.objects.remove( o, do_unlink=True ) 195 | 196 | 197 | class MESH_OT_quickbooladd( bpy.types.Operator ): 198 | bl_idname = 'view3d.rm_quickbooladd' 199 | bl_label = 'Add' 200 | bl_options = { 'UNDO' } 201 | 202 | operation: bpy.props.EnumProperty( 203 | items=[ ( 'UNION', "Union", "", 1 ), 204 | ( 'DIFFERENCE', "Subtract", "", 2 ), 205 | ( 'INTERSECT', "Intersect", "", 3 ) ], 206 | name="Boolean Type", 207 | default='UNION' 208 | ) 209 | 210 | @classmethod 211 | def poll( cls, context ): 212 | return ( context.area.type == 'VIEW_3D' and 213 | context.active_object is not None and 214 | context.active_object.type == 'MESH' and 215 | context.mode == 'OBJECT' ) 216 | 217 | def execute( self, context ): 218 | #make active object a parrent 219 | obj_active = context.active_object 220 | if not IsParent( obj_active ): 221 | MakeParent( obj_active ) 222 | 223 | #all other selected objects are drive meshes 224 | for o in context.selected_objects: 225 | if o.type != 'MESH' or o == obj_active: 226 | continue 227 | AddBoolean( o, obj_active, self.operation ) 228 | 229 | CleanupQuickBool( context ) 230 | 231 | return { 'FINISHED' } 232 | 233 | 234 | class MESH_OT_quickboolclear( bpy.types.Operator ): 235 | bl_idname = 'view3d.rm_quickboolclear' 236 | bl_label = 'Remove' 237 | bl_options = { 'UNDO' } 238 | 239 | @classmethod 240 | def poll( cls, context ): 241 | return ( context.area.type == 'VIEW_3D' and 242 | context.active_object is not None and 243 | context.active_object.type == 'MESH' and 244 | context.mode == 'OBJECT' ) 245 | 246 | def execute( self, context ): 247 | #make active object a parent 248 | obj_active = context.active_object 249 | if IsParent( obj_active ): 250 | for m in list( obj_active.modifiers ): 251 | if m.type == 'BOOLEAN' and m.name.startswith( BOOL_DUO_KEY ) and m.object is not None: 252 | duo = DUO_Wrapper( m.object ) 253 | for o in list( duo.collection.objects ): 254 | duo.collection.objects.unlink( o ) 255 | context.scene.collection.objects.link( o ) 256 | o.display_type = 'TEXTURED' 257 | del o[BOOL_CHILD_KEY] 258 | bpy.data.objects.remove( m.object, do_unlink=True ) 259 | obj_active.modifiers.remove( m ) 260 | del obj_active[BOOL_PARENT_KEY] 261 | elif IsChild( obj_active ): 262 | collections_mod = BoolCollectionManager( context.scene ) 263 | collections_mod.ClearObjectFromCollections( obj_active ) 264 | del obj_active[BOOL_CHILD_KEY] 265 | else: 266 | return { 'FINISHED' } 267 | 268 | CleanupQuickBool( context ) 269 | 270 | return { 'FINISHED' } 271 | 272 | 273 | class VIEW3D_MT_quickbool( bpy.types.Menu ): 274 | bl_idname = 'VIEW3D_MT_quickbool' 275 | bl_label = 'Quick Boolean GUI' 276 | 277 | def draw( self, context ): 278 | layout = self.layout 279 | 280 | if context.active_object.type == 'MESH' and context.mode == 'EDIT_MESH': 281 | sel_mode = context.tool_settings.mesh_select_mode[:] 282 | if sel_mode[2]: 283 | layout.operator( 'mesh.intersect_boolean', text='Union', icon="SELECT_EXTEND" ).operation = 'UNION' 284 | layout.operator( 'mesh.intersect_boolean', text='Difference', icon="SELECT_SUBTRACT" ).operation = 'DIFFERENCE' 285 | layout.operator( 'mesh.intersect_boolean', text='Intersect', icon="SELECT_INTERSECT" ).operation = 'INTERSECT' 286 | layout.operator( 'mesh.intersect', text='Slice', icon="SELECT_DIFFERENCE" ).mode = 'SELECT_UNSELECT' 287 | 288 | 289 | def register(): 290 | bpy.utils.register_class( VIEW3D_MT_quickbool ) 291 | 292 | 293 | def unregister(): 294 | bpy.utils.unregister_class( VIEW3D_MT_quickbool ) -------------------------------------------------------------------------------- /quickmaterial.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh, mathutils, bpy_extras 2 | import string 3 | import rmlib 4 | 5 | MAT_PROP_UPDATED = False 6 | 7 | def validate_material_name( mat_name ): 8 | if mat_name == '': 9 | return False 10 | valid_chars = "-_(){}{}\\".format( string.ascii_letters, string.digits ) 11 | for c in mat_name: 12 | if c not in valid_chars: 13 | return False 14 | return True 15 | 16 | 17 | class MESH_OT_rm_matcleanup( bpy.types.Operator ): 18 | """Cleanup materials and material_indexes on mesh""" 19 | bl_idname = 'mesh.rm_matclearnup' 20 | bl_label = 'Material Cleanup' 21 | bl_options = { 'UNDO' } 22 | 23 | @classmethod 24 | def poll( cls, context ): 25 | return ( context.area.type == 'VIEW_3D' and 26 | context.mode == 'OBJECT' and 27 | context.object is not None and 28 | context.object.type == 'MESH' and 29 | context.object is not None ) 30 | 31 | def execute( self, context ): 32 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=False ): 33 | #get mesh material list 34 | old_materials = [ m for m in rmmesh.mesh.materials ] 35 | if len( old_materials ) == 0: 36 | continue 37 | 38 | #remap 39 | new_materials = [] 40 | with rmmesh as rmmesh: 41 | overflow_faces = set() 42 | for p in rmmesh.bmesh.faces: 43 | if p.material_index >= len( old_materials ): 44 | overflow_faces.add( p ) 45 | continue 46 | 47 | idx = p.material_index 48 | try: 49 | p.material_index = new_materials.index( old_materials[idx] ) #gets rid of duplicates 50 | except ValueError: 51 | p.material_index = len( new_materials ) 52 | new_materials.append( old_materials[idx] ) 53 | 54 | for f in overflow_faces: 55 | f.material_index = len( new_materials ) - 1 56 | 57 | rmmesh.mesh.materials.clear() 58 | for m in new_materials: 59 | rmmesh.mesh.materials.append( m ) 60 | 61 | return { 'FINISHED' } 62 | 63 | 64 | class MESH_OT_quickmaterial( bpy.types.Operator ): 65 | """Utility for quickly sampling, modifying, and creating materials for 3d viewport.""" 66 | bl_idname = 'mesh.rm_quickmaterial' 67 | bl_label = 'Quick Material' 68 | bl_options = { 'UNDO' } 69 | 70 | new_name: bpy.props.StringProperty( name='Name' ) 71 | 72 | @classmethod 73 | def poll( cls, context ): 74 | return context.area.type == 'VIEW_3D' 75 | 76 | def execute( self, context ): 77 | sel_mode = context.tool_settings.mesh_select_mode[:] 78 | if not sel_mode[2]: 79 | return { 'CANCELLED' } 80 | 81 | material = bpy.context.scene.quickmatprops['prop_mat'] 82 | if material is None: 83 | mat_name = self.new_name.strip().replace( '/', '\\' ) 84 | if not validate_material_name( mat_name ): 85 | self.report({'ERROR'}, 'Material name contains invalid characters.' ) 86 | return { 'CANCELLED' } 87 | material = bpy.data.materials.new( name=mat_name ) 88 | material.use_nodes = True 89 | 90 | global MAT_PROP_UPDATED 91 | if MAT_PROP_UPDATED: 92 | material.diffuse_color = bpy.context.scene.quickmatprops['prop_col'] 93 | material.metallic = bpy.context.scene.quickmatprops['prop_met'] 94 | material.roughness = bpy.context.scene.quickmatprops['prop_rog'] 95 | material['WorldMappingWidth'] = bpy.context.scene.quickmatprops['prop_width'] 96 | material['WorldMappingHeight'] = bpy.context.scene.quickmatprops['prop_height'] 97 | 98 | node_tree = material.node_tree 99 | nodes = node_tree.nodes 100 | bsdf = nodes.get('Principled BSDF') 101 | if bsdf: 102 | bsdf.inputs[0].default_value = bpy.context.scene.quickmatprops['prop_col'] 103 | bsdf.inputs[2].default_value = bpy.context.scene.quickmatprops['prop_rog'] 104 | bsdf.inputs[6].default_value = bpy.context.scene.quickmatprops['prop_met'] 105 | 106 | if not context.object.data.is_editmode: 107 | return { 'FINISHED' } 108 | 109 | if context.object is None or context.object.type != 'MESH': 110 | return { 'FINISHED' } 111 | 112 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 113 | bm = bmesh.from_edit_mesh( rmmesh.mesh ) 114 | rmmesh.bmesh = bm 115 | 116 | match_found = False 117 | for i, mat in enumerate( rmmesh.mesh.materials ): 118 | if mat is not None and mat.name_full == material.name_full: 119 | match_found = True 120 | break 121 | if match_found: 122 | selected_polys = rmlib.rmPolygonSet.from_selection( rmmesh ) 123 | for p in selected_polys: 124 | p.material_index = i 125 | else: 126 | rmmesh.mesh.materials.append( material ) 127 | selected_polys = rmlib.rmPolygonSet.from_selection( rmmesh ) 128 | for p in selected_polys: 129 | p.material_index = len( rmmesh.mesh.materials ) - 1 130 | 131 | bmesh.update_edit_mesh( rmmesh.mesh, loop_triangles=False, destructive=False ) 132 | 133 | return { 'FINISHED' } 134 | 135 | def draw( self, context ): 136 | layout= self.layout 137 | layout.prop( context.scene.quickmatprops, 'prop_mat' ) 138 | layout.separator( factor=0.1 ) 139 | box = layout.box() 140 | material = bpy.context.scene.quickmatprops['prop_mat'] 141 | if material is None: 142 | box.prop( self, 'new_name' ) 143 | box.prop( context.scene.quickmatprops, 'prop_col' ) 144 | box.prop( context.scene.quickmatprops, 'prop_met' ) 145 | box.prop( context.scene.quickmatprops, 'prop_rog' ) 146 | box.prop( context.scene.quickmatprops, 'prop_width' ) 147 | box.prop( context.scene.quickmatprops, 'prop_height' ) 148 | layout.separator( factor=1 ) 149 | 150 | def invoke( self, context, event ): 151 | m_x, m_y = event.mouse_region_x, event.mouse_region_y 152 | mouse_pos = mathutils.Vector( ( float( m_x ), float( m_y ) ) ) 153 | 154 | look_pos = bpy_extras.view3d_utils.region_2d_to_origin_3d( context.region, context.region_data, mouse_pos ) 155 | look_vec = bpy_extras.view3d_utils.region_2d_to_vector_3d( context.region, context.region_data, mouse_pos ) 156 | 157 | depsgraph = context.evaluated_depsgraph_get() 158 | depsgraph.update() 159 | hit, loc, nml, idx, obj, mat = context.scene.ray_cast( depsgraph, look_pos, look_vec ) 160 | set_defaults = True 161 | if hit and len( obj.data.materials ) > 0: 162 | mat_idx = 0 163 | rmmesh = rmlib.rmMesh( obj ) 164 | with rmmesh as rmmesh: 165 | rmmesh.readonly = True 166 | try: 167 | source_poly = rmlib.rmPolygonSet.from_mos( rmmesh, context, mouse_pos )[0] 168 | except IndexError: 169 | print( 'ERROR :: QuickMat INVOKE :: from_mos failed' ) 170 | return { 'CANCELLED' } 171 | mat_idx = source_poly.material_index 172 | try: 173 | material = obj.data.materials[mat_idx] 174 | except IndexError: 175 | material = obj.data.materials[0] 176 | if material is None: 177 | return context.window_manager.invoke_props_dialog( self, width=230 ) 178 | bpy.context.scene.quickmatprops['prop_mat'] = material 179 | bpy.context.scene.quickmatprops['prop_col'] = material.diffuse_color 180 | bpy.context.scene.quickmatprops['prop_met'] = material.metallic 181 | bpy.context.scene.quickmatprops['prop_rog'] = material.roughness 182 | try: 183 | bpy.context.scene.quickmatprops['prop_width'] = material['WorldMappingWidth'] 184 | bpy.context.scene.quickmatprops['prop_height'] = material['WorldMappingHeight'] 185 | except KeyError: 186 | bpy.context.scene.quickmatprops['prop_width'] = 2.0 187 | bpy.context.scene.quickmatprops['prop_height'] = 2.0 188 | else: 189 | bpy.context.scene.quickmatprops['prop_mat'] = None 190 | bpy.context.scene.quickmatprops['prop_col'] = ( 0.5, 0.5, 0.5, 1.0 ) 191 | bpy.context.scene.quickmatprops['prop_met'] = 0.0 192 | bpy.context.scene.quickmatprops['prop_rog'] = 0.4 193 | bpy.context.scene.quickmatprops['prop_width'] = 2.0 194 | bpy.context.scene.quickmatprops['prop_height'] = 2.0 195 | 196 | return context.window_manager.invoke_props_dialog( self, width=230 ) 197 | 198 | 199 | def mat_search_changed( self, context ): 200 | global MAT_PROP_UPDATED 201 | MAT_PROP_UPDATED = False 202 | material = self['prop_mat'] 203 | if material is not None: 204 | self['prop_col'] = material.diffuse_color 205 | self['prop_met'] = material.metallic 206 | self['prop_rog'] = material.roughness 207 | try: 208 | self['prop_width'] = material['WorldMappingWidth'] 209 | self['prop_height'] = material['WorldMappingHeight'] 210 | except KeyError: 211 | self['prop_width'] = 2.0 212 | self['prop_height'] = 2.0 213 | else: 214 | self['prop_col'] = ( 0.5, 0.5, 0.5, 1.0 ) 215 | self['prop_met'] = 0.0 216 | self['prop_rog'] = 0.4 217 | self['prop_width'] = 2.0 218 | self['prop_height'] = 2.0 219 | 220 | 221 | def mat_prop_changed( self, context ): 222 | global MAT_PROP_UPDATED 223 | MAT_PROP_UPDATED = True 224 | 225 | 226 | class QuickMatProps( bpy.types.PropertyGroup ): 227 | prop_mat: bpy.props.PointerProperty( name='Material', type=bpy.types.Material, update=lambda self, context : mat_search_changed( self, context ) ) 228 | prop_col: bpy.props.FloatVectorProperty( name='Color', subtype= 'COLOR_GAMMA', size=4, default=( 0.5, 0.5, 0.5, 1.0 ), update=lambda self, context : mat_prop_changed( self, context ) ) 229 | prop_met: bpy.props.FloatProperty( name='Metallic', default=0.0, min=0.0, max=1.0, update=lambda self, context : mat_prop_changed( self, context ) ) 230 | prop_rog: bpy.props.FloatProperty( name='Roughness', default=0.4, min=0.0, max=1.0, update=lambda self, context : mat_prop_changed( self, context ) ) 231 | prop_width: bpy.props.FloatProperty( name='World Width', default=2.0, min=0.0, subtype='DISTANCE', unit='LENGTH', update=lambda self, context : mat_prop_changed( self, context ) ) 232 | prop_height: bpy.props.FloatProperty( name='World Height', default=2.0, min=0.0, subtype='DISTANCE', unit='LENGTH', update=lambda self, context : mat_prop_changed( self, context ) ) 233 | 234 | 235 | def register(): 236 | bpy.utils.register_class( MESH_OT_quickmaterial ) 237 | bpy.utils.register_class( QuickMatProps ) 238 | bpy.types.Scene.quickmatprops = bpy.props.PointerProperty( type=QuickMatProps ) 239 | bpy.utils.register_class( MESH_OT_rm_matcleanup ) 240 | 241 | def unregister(): 242 | bpy.utils.unregister_class( MESH_OT_quickmaterial ) 243 | bpy.utils.unregister_class( QuickMatProps ) 244 | del bpy.types.Scene.quickmatprops 245 | bpy.utils.unregister_class( MESH_OT_rm_matcleanup ) -------------------------------------------------------------------------------- /radial_align.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import rmlib 4 | import mathutils 5 | import math 6 | 7 | def circularize( vert_loop, matrix ): 8 | vcount = len( vert_loop ) 9 | pos_loop = [ v.co.copy() for v in vert_loop ] 10 | 11 | #init delta vec by findin a vec that most aligns with a grid axis. 12 | #this vec is a sum of the cross vecs. each cross is between a boudary edge vec and the axis of rotation. 13 | #we skip baundary edge vecs that are too closely aligned with the prev one. 14 | rot_axis = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 15 | center = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 16 | prev_vec = ( vert_loop[0].co - vert_loop[-1].co ).normalized() 17 | prev_vert = vert_loop[-1] 18 | max_dot = -1.0 19 | delta_vec_before_first_rot = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 20 | delta_vec_verts = [] 21 | count = 0 22 | for i in range( vcount ): 23 | curr_vec = ( pos_loop[(i+1)%vcount] - pos_loop[i] ).normalized() 24 | if curr_vec.angle( prev_vec ) <= math.radians( 0.1 ): 25 | continue 26 | rot_axis += prev_vec.cross( curr_vec ) 27 | center += pos_loop[i] 28 | prev_vec = curr_vec 29 | count += 1 30 | 31 | vec_out = ( pos_loop[i-1] - pos_loop[i] ).normalized() 32 | vec_out = vec_out.cross( rot_axis ).normalized() 33 | for j in range( 3 ): 34 | dot_out = abs( vec_out.dot( matrix[j] ) ) 35 | if dot_out >= max_dot: 36 | max_dot = dot_out 37 | delta_vec_verts = [prev_vert, vert_loop[i]] 38 | delta_vec_before_first_rot = vec_out 39 | 40 | prev_vert = vert_loop[i] 41 | 42 | rot_axis.normalize() 43 | center *= 1.0 / count 44 | radius = 0.0 45 | for p in pos_loop: 46 | radius += ( p - center ).length 47 | radius *= 1.0 / float( vcount ) 48 | 49 | #set new vert positions 50 | rot_quat = mathutils.Quaternion( rot_axis, math.pi * 2.0 / vcount ) 51 | v = ( pos_loop[0] - center ).normalized() 52 | vert_loop[0].co = center + v * radius 53 | for i in range( 1, vcount ): 54 | v.rotate( rot_quat ) 55 | vert_loop[i].co = center + v * radius 56 | 57 | #get the value of delta vec after initial rotation 58 | delta_vec_after_first_rot = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 59 | if len( delta_vec_verts ) == 1: 60 | delta_vec_after_first_rot = ( delta_vec_verts[0].co - center ).normalized() 61 | else: 62 | delta_vec_after_first_rot = ( delta_vec_verts[0].co - delta_vec_verts[1].co ).normalized() 63 | delta_vec_after_first_rot = delta_vec_after_first_rot.cross( rot_axis ).normalized() 64 | 65 | #apply rotation from delta_vec to all verts to axis align the loop 66 | theta = rmlib.util.Angle2( delta_vec_before_first_rot, delta_vec_after_first_rot, rot_axis ) 67 | rot_quat = mathutils.Quaternion( rot_axis, -theta ) 68 | for i in range( vcount ): 69 | v = ( vert_loop[i].co - center ).normalized() 70 | v.rotate( rot_quat ) 71 | vert_loop[i].co = center + v * radius 72 | 73 | 74 | class MESH_OT_radialalign( bpy.types.Operator ): 75 | """Map the verts that make up the edge selection or boundary of face selection to a circle.""" 76 | bl_idname = 'mesh.rm_radialalign' 77 | bl_label = 'Radial Align' 78 | bl_options = { 'UNDO' } 79 | 80 | @classmethod 81 | def poll( cls, context ): 82 | return ( context.area.type == 'VIEW_3D' and 83 | context.active_object is not None and 84 | context.active_object.type == 'MESH' and 85 | context.object.data.is_editmode ) 86 | 87 | def execute( self, context ): 88 | sel_mode = context.tool_settings.mesh_select_mode[:] 89 | if sel_mode[0]: 90 | return { 'CANCELLED' } 91 | 92 | rm_wp = rmlib.rmCustomOrientation.from_selection( context ) 93 | rmmesh = rmlib.rmMesh.GetActive( context ) 94 | with rmmesh as rmmesh: 95 | if sel_mode[1]: 96 | edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 97 | elif sel_mode[2]: 98 | polys = rmlib.rmPolygonSet.from_selection( rmmesh ) 99 | edges = rmlib.rmEdgeSet() 100 | for p in polys: 101 | for e in p.edges: 102 | if e.is_boundary: 103 | edges.append( e ) 104 | continue 105 | has_unselected_neigh = False 106 | for n_p in e.link_faces: 107 | if n_p != p and not n_p.select and e not in edges: 108 | edges.append( e ) 109 | break 110 | 111 | for chain in edges.chain(): 112 | if chain[0][0] != chain[-1][-1]: 113 | continue 114 | 115 | vert_loop = [ pair[0] for pair in chain ] 116 | circularize( vert_loop, rm_wp.matrix ) 117 | 118 | return { 'FINISHED' } 119 | 120 | def register(): 121 | bpy.utils.register_class( MESH_OT_radialalign ) 122 | 123 | def unregister(): 124 | bpy.utils.unregister_class( MESH_OT_radialalign ) -------------------------------------------------------------------------------- /reduce.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh, mathutils 2 | import rmlib 3 | 4 | def FindStartLoop( faces ): 5 | for f in faces: 6 | for l in f.loops: 7 | if not l.vert.tag: 8 | return l 9 | return None 10 | 11 | 12 | def UnselectedEdgeCount( loop ): 13 | unselected_edges = 0 14 | for e in loop.vert.link_edges: 15 | if not e.select: 16 | unselected_edges += 1 17 | return unselected_edges 18 | 19 | 20 | def CreateFace( rmmesh, loop_list, proto ): 21 | if len( loop_list ) > 2 and len( loop_list ) == len( set( loop_list ) ): 22 | try: 23 | new_face = rmmesh.bmesh.faces.new( [l.vert for l in loop_list], proto ) 24 | except ValueError: 25 | return 26 | 27 | #copy over uv data 28 | if rmmesh.bmesh.loops.layers.uv is not None: 29 | for uvlayer in rmmesh.bmesh.loops.layers.uv.values(): 30 | for i, new_face_loop in enumerate( new_face.loops ): 31 | new_face_loop[uvlayer].uv = loop_list[i][uvlayer].uv 32 | 33 | #copy over vcolor data 34 | if rmmesh.bmesh.loops.layers.color is not None: 35 | for colorlayer in rmmesh.bmesh.loops.layers.color: 36 | for i, new_face_loop in enumerate( new_face.loops ): 37 | new_face_loop[colorlayer] = loop_list[i][colorlayer] 38 | 39 | #edge data processing 40 | clyr = rmmesh.bmesh.edges.layers.float.get( 'crease_edge', None ) 41 | if clyr is None: 42 | clyr = rmmesh.bmesh.edges.layers.float.get( 'crease_edge' ) 43 | 44 | blyr = rmmesh.bmesh.edges.layers.float.get( 'bevel_weight_edge', None ) 45 | if blyr is None: 46 | blyr = rmmesh.bmesh.edges.layers.float.get( 'bevel_weight_edge' ) 47 | 48 | for i, new_face_loop in enumerate( new_face.loops ): 49 | if clyr is not None: 50 | new_face_loop.edge[clyr] = loop_list[i].edge[clyr] 51 | if blyr is not None: 52 | new_face_loop.edge[blyr] = loop_list[i].edge[blyr] 53 | new_face_loop.edge.smooth = loop_list[i].edge.smooth 54 | new_face_loop.edge.seam = loop_list[i].edge.seam 55 | 56 | 57 | def GetNewPolygonLoopList( loop_list, loop, first_loop ): 58 | loop.face.tag = True 59 | 60 | #determine if current vert get appended 61 | if loop.vert.tag: 62 | if UnselectedEdgeCount( loop ) > 2: 63 | loop_list.append( loop ) 64 | else: 65 | loop_list.append( loop ) 66 | 67 | #determine which is the next loop 68 | if loop.vert.tag: 69 | while( loop.edge.select ): 70 | loop = loop.link_loop_radial_next.link_loop_next 71 | else: 72 | if loop.link_loop_next.vert.tag and UnselectedEdgeCount( loop.link_loop_next ) == 1: 73 | loop = loop.link_loop_radial_next.link_loop_next 74 | 75 | if first_loop.vert == loop.link_loop_next.vert: 76 | return loop_list 77 | 78 | return GetNewPolygonLoopList( loop_list, loop.link_loop_next, first_loop ) 79 | 80 | 81 | def ClearTags( rmmesh ): 82 | for v in rmmesh.bmesh.verts: 83 | v.tag = False 84 | for f in rmmesh.bmesh.faces: 85 | f.tag = False 86 | 87 | 88 | def pop_edges( rmmesh, sel_edges ): 89 | ClearTags( rmmesh ) 90 | 91 | for v in sel_edges.vertices: 92 | v.tag = True 93 | 94 | active_faces = sel_edges.polygons 95 | 96 | cap_faces = rmlib.rmPolygonSet() 97 | for f in sel_edges.vertices.polygons: 98 | sel_edge_count = 0 99 | for e in f.edges: 100 | if e.select: 101 | sel_edge_count += 1 102 | 103 | tag_vert_count = 0 104 | for v in f.verts: 105 | if v.tag: 106 | tag_vert_count += 1 107 | 108 | if sel_edge_count == 0 and tag_vert_count > 0: 109 | cap_faces.append( f ) 110 | 111 | next_active_faces = active_faces.copy() 112 | while( len( next_active_faces ) > 0 ): 113 | start_loop = FindStartLoop( next_active_faces ) 114 | if start_loop is None: 115 | break 116 | 117 | loop_list = GetNewPolygonLoopList( [], start_loop, start_loop ) 118 | 119 | CreateFace( rmmesh, loop_list, start_loop.face ) 120 | 121 | next_active_faces.clear() 122 | for f in active_faces: 123 | if not f.tag: 124 | next_active_faces.append( f ) 125 | 126 | for f in cap_faces: 127 | loop_list = [] 128 | for l in f.loops: 129 | if l.vert.tag: 130 | if UnselectedEdgeCount( l ) > 2: 131 | loop_list.append( l ) 132 | else: 133 | loop_list.append( l ) 134 | if len( loop_list ) < len( f.loops ): 135 | CreateFace( rmmesh, loop_list, f ) 136 | active_faces.append( f ) 137 | 138 | bmesh.ops.delete( rmmesh.bmesh, geom=active_faces, context='FACES' ) 139 | 140 | ClearTags( rmmesh ) 141 | 142 | def has_open_vert_member( verts ): 143 | for v in verts: 144 | for e in v.link_edges: 145 | if e.is_boundary: 146 | return True 147 | return False 148 | 149 | def collapse_verts( verts ): 150 | collapse_groups = [ [], ] 151 | for g in verts.group(): 152 | if has_open_vert_member( g ): 153 | collapse_groups[0] += g 154 | else: 155 | collapse_groups.append( g ) 156 | 157 | for g in collapse_groups: 158 | if len( g ) < 1: 159 | continue 160 | avg = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 161 | for v in g: 162 | avg += v.co 163 | avg *= 1.0 / len( g ) 164 | for v in g: 165 | v.co = avg 166 | 167 | class MESH_OT_reduce( bpy.types.Operator ): 168 | """Delete/Remove/Collapse selected components.""" 169 | bl_idname = 'mesh.rm_remove' 170 | bl_label = 'Reduce Selection' 171 | #bl_options = { 'REGISTER', 'UNDO' } 172 | bl_options = { 'UNDO' } 173 | 174 | reduce_mode: bpy.props.EnumProperty( 175 | items=[ ( "DEL", "Delete", "", 1 ), 176 | ( "COL", "Collapse", "", 2 ), 177 | ( "DIS", "Dissolve", "", 3 ), 178 | ( "POP", "Pop", "", 4 ) ], 179 | name="Reduce Mode", 180 | default="DEL" 181 | ) 182 | 183 | @classmethod 184 | def poll( cls, context ): 185 | return ( context.area.type == 'VIEW_3D' and 186 | context.active_object is not None and 187 | context.active_object.type == 'MESH' and 188 | context.active_object.data.is_editmode ) 189 | 190 | def execute( self, context ): 191 | sel_mode = context.tool_settings.mesh_select_mode[:] 192 | 193 | empty_selection = True 194 | 195 | for rmmesh in rmlib.item.iter_edit_meshes( context ): 196 | if sel_mode[0]: #vert mode 197 | with rmmesh as rmmesh: 198 | rmmesh.readonly = True 199 | sel_verts = rmlib.rmVertexSet.from_selection( rmmesh ) 200 | if len( sel_verts ) > 0: 201 | empty_selection = False 202 | if self.reduce_mode == 'DEL': 203 | bpy.ops.mesh.delete( type='VERT' ) 204 | elif self.reduce_mode == 'COL': 205 | rmmesh.readonly = False 206 | verts = rmlib.rmVertexSet.from_selection( rmmesh ) 207 | collapse_verts( verts ) 208 | bmesh.ops.remove_doubles( rmmesh.bmesh, verts=verts, dist=0.00001 ) 209 | else: 210 | bpy.ops.mesh.dissolve_verts() 211 | 212 | if sel_mode[1]: #edge mode 213 | with rmmesh as rmmesh: 214 | sel_edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 215 | if len( sel_edges ) > 0: 216 | empty_selection = False 217 | if self.reduce_mode == 'DEL': 218 | lone_edges = [ e for e in sel_edges if len( e.link_faces ) == 0 ] 219 | bmesh.ops.delete( rmmesh.bmesh, geom=sel_edges.polygons , context='FACES' ) 220 | bmesh.ops.delete( rmmesh.bmesh, geom=lone_edges , context='EDGES' ) 221 | 222 | elif self.reduce_mode == 'COL': 223 | bpy.ops.mesh.edge_collapse() 224 | elif self.reduce_mode == 'DIS': 225 | bpy.ops.mesh.dissolve_edges( use_verts=False, use_face_split=False ) 226 | else: #'POP' 227 | active_edges = rmlib.rmEdgeSet() 228 | for e in sel_edges: 229 | if e.is_boundary: 230 | e.select = False 231 | else: 232 | active_edges.append( e ) 233 | if len( active_edges ) > 0: 234 | pop_edges( rmmesh, active_edges ) 235 | 236 | if sel_mode[2]: #poly mode 237 | with rmmesh as rmmesh: 238 | rmmesh.readonly = True 239 | sel_polys = rmlib.rmPolygonSet.from_selection( rmmesh ) 240 | if len( sel_polys ) > 0: 241 | empty_selection = False 242 | if self.reduce_mode == 'COL': 243 | bpy.ops.mesh.edge_collapse() 244 | else: 245 | bpy.ops.mesh.delete( type='FACE' ) 246 | 247 | if empty_selection: 248 | self.report( { 'INFO' }, 'Empty selection' ) 249 | return { 'CANCELLED' } 250 | 251 | return { 'FINISHED' } 252 | 253 | def register(): 254 | bpy.utils.register_class( MESH_OT_reduce ) 255 | 256 | def unregister(): 257 | bpy.utils.unregister_class( MESH_OT_reduce ) -------------------------------------------------------------------------------- /screenreflect.py: -------------------------------------------------------------------------------- 1 | import math 2 | import bpy, bmesh, mathutils 3 | import rmlib 4 | 5 | class MESH_OT_screenreflect( bpy.types.Operator ): 6 | """Reflect polygon selection based on relative screen direction.""" 7 | bl_idname = 'mesh.rm_screenreflect' 8 | bl_label = 'ScreenReflect' 9 | bl_options = { 'UNDO' } 10 | 11 | str_dir: bpy.props.EnumProperty( 12 | items=[ ( "up", "Up", "", 1 ), 13 | ( "down", "Down", "", 2 ), 14 | ( "left", "Left", "", 3 ), 15 | ( "right", "Right", "", 4 ) ], 16 | name="Direction", 17 | default="right" 18 | ) 19 | 20 | mode: bpy.props.IntProperty( 21 | name='Mode', 22 | description='0::Reflect about fartest point of mesh on desired axis. 1::Slice and mirror about cursor pos. 2::Mirror about cursor pos.' 23 | ) 24 | 25 | @classmethod 26 | def poll( cls, context ): 27 | return ( context.area.type == 'VIEW_3D' ) 28 | 29 | def execute( self, context ): 30 | if context.object is None: 31 | return { 'CANCELLED' } 32 | 33 | if context.object.type != 'MESH': 34 | return { 'CANCELLED' } 35 | 36 | rm_vp = rmlib.rmViewport( context ) 37 | rm_wp = rmlib.rmCustomOrientation.from_selection( context ) 38 | 39 | if context.mode == 'EDIT_MESH': 40 | sel_mode = context.tool_settings.mesh_select_mode[:] 41 | if not sel_mode[2]: 42 | return { 'CANCELLED' } 43 | 44 | #retrieve active mesh 45 | rmmesh = rmlib.rmMesh.GetActive( context ) 46 | with rmmesh as rmmesh: 47 | #get selected polyons and init geom list for slicing 48 | active_polys = rmlib.rmPolygonSet.from_selection( rmmesh ) 49 | if len( active_polys ) < 1: 50 | return { 'CANCELLED' } 51 | active_verts = active_polys.vertices 52 | geom = [] 53 | geom.extend( active_polys.vertices ) 54 | geom.extend( active_polys.edges ) 55 | geom.extend( active_polys ) 56 | 57 | dir_idx, cam_dir_vec, grid_dir_vec = rm_vp.get_nearest_direction_vector( self.str_dir, rm_wp.matrix ) 58 | 59 | inv_rot_mat = rmmesh.world_transform.to_3x3().inverted() 60 | grid_dir_vec = inv_rot_mat @ grid_dir_vec 61 | 62 | if self.mode == 0: 63 | #find the farthest point in the direction of the desired axis aligned direction 64 | reflection_center = active_verts[0].co.copy() 65 | max_dot = grid_dir_vec.dot( active_verts[0].co ) 66 | for i in range( 1, len( active_verts ) ): 67 | v = active_verts[i] 68 | dot = grid_dir_vec.dot( v.co ) 69 | if dot > max_dot: 70 | max_dot = dot 71 | reflection_center = v.co.copy() 72 | 73 | elif self.mode == 1: 74 | #slice geo and delete everything on outer side of slice plane 75 | cursor_pos = mathutils.Vector( bpy.context.scene.cursor.location ) 76 | reflection_center = rmmesh.world_transform.inverted() @ cursor_pos# @ rmmesh.world_transform 77 | d = bmesh.ops.bisect_plane( rmmesh.bmesh, geom=geom, dist=0.00001, plane_co=reflection_center, plane_no=grid_dir_vec, use_snap_center=False, clear_outer=True, clear_inner=False ) 78 | geom = d[ 'geom' ] 79 | 80 | elif self.mode == 2: 81 | cursor_pos = mathutils.Vector( bpy.context.scene.cursor.location ) 82 | reflection_center = rmmesh.world_transform.inverted() @ cursor_pos# @ rmmesh.world_transform 83 | 84 | #mirror selection across reflection/slice plane 85 | reflection = rmlib.util.ReflectionMatrix( reflection_center, grid_dir_vec ) 86 | d = bmesh.ops.duplicate( rmmesh.bmesh, geom=geom ) 87 | rev_faces = [] 88 | for elem in d['geom']: 89 | if isinstance( elem, bmesh.types.BMVert ): 90 | elem.co = reflection @ elem.co 91 | elif isinstance( elem, bmesh.types.BMFace ): 92 | rev_faces.append( elem ) 93 | bmesh.ops.reverse_faces( rmmesh.bmesh, faces=rev_faces ) 94 | 95 | if self.mode == 1: 96 | #merge vertices that are on the slice plane 97 | epsilon = 0.0001 98 | merge_verts = rmlib.rmVertexSet() 99 | for v in rmmesh.bmesh.verts: 100 | if abs( rmlib.util.PlaneDistance( v.co, reflection_center, grid_dir_vec ) ) < epsilon: 101 | merge_verts.append( v ) 102 | bmesh.ops.remove_doubles( rmmesh.bmesh, verts=merge_verts, dist=epsilon ) 103 | 104 | elif context.mode == 'OBJECT': 105 | dir_idx, cam_dir_vec, grid_dir_vec = rm_vp.get_nearest_direction_vector( self.str_dir, rm_wp.matrix ) 106 | 107 | obj_selection = list( bpy.context.selected_objects ) 108 | 109 | if self.mode == 0: 110 | reflection_center = None 111 | for obj in obj_selection: 112 | if obj.type != 'MESH': 113 | continue 114 | 115 | rmmesh = rmlib.rmMesh( obj ) 116 | mat = rmmesh.world_transform 117 | inv_mat = mat.inverted() 118 | inv_rot_mat = mat.to_3x3().inverted() 119 | with rmmesh as rmmesh: 120 | rmmesh.readonly = True 121 | 122 | grid_dir_vec_objspc = inv_rot_mat @ grid_dir_vec 123 | 124 | #find the farthest point in the direction of the desired axis aligned direction 125 | active_verts = rmlib.rmVertexSet.from_mesh( rmmesh=rmmesh, filter_hidden=True ) 126 | rc = active_verts[0].co.copy() 127 | max_dot = grid_dir_vec_objspc.dot( active_verts[0].co ) 128 | for i in range( 1, len( active_verts ) ): 129 | v = active_verts[i] 130 | dot = grid_dir_vec_objspc.dot( v.co ) 131 | if dot > max_dot: 132 | max_dot = dot 133 | rc = v.co.copy() 134 | if reflection_center is not None: 135 | cur_rc = inv_mat @ reflection_center 136 | dot = grid_dir_vec_objspc.dot( cur_rc ) 137 | if dot > max_dot: 138 | rc = cur_rc 139 | 140 | reflection_center = mat @ rc 141 | 142 | reflection = rmlib.util.ReflectionMatrix( reflection_center, grid_dir_vec ) 143 | for obj in obj_selection: 144 | mat = mathutils.Matrix( obj.matrix_world ) 145 | new_mat = reflection @ mat 146 | 147 | new_obj = bpy.data.objects.new( obj.name, obj.data ) 148 | obj.users_collection[0].objects.link( new_obj ) 149 | new_obj.matrix_world = new_mat 150 | 151 | else: 152 | reflection_center = mathutils.Vector( bpy.context.scene.cursor.location ) 153 | reflection = rmlib.util.ReflectionMatrix( reflection_center, grid_dir_vec ) 154 | 155 | for obj in obj_selection: 156 | mat = mathutils.Matrix( obj.matrix_world ) 157 | new_mat = reflection @ mat 158 | 159 | new_obj = bpy.data.objects.new( obj.name, obj.data ) 160 | obj.users_collection[0].objects.link( new_obj ) 161 | new_obj.matrix_world = new_mat 162 | 163 | return { 'FINISHED' } 164 | 165 | 166 | class VIEW3D_MT_PIE_screenreflect( bpy.types.Menu ): 167 | """Quickly mirror the face selection about a plane whose normal is defined by a grid direction relative to viewport camera.""" 168 | bl_idname = 'OBJECT_MT_rm_screenreflect' 169 | bl_label = 'Screen Reflect' 170 | 171 | def draw( self, context ): 172 | layout = self.layout 173 | 174 | pie = layout.menu_pie() 175 | 176 | op_l = pie.operator( MESH_OT_screenreflect.bl_idname, text='Left' ) 177 | op_l.str_dir = 'left' 178 | op_l.mode = context.scene.rmkit_props.screenreflectprops.sr_0 179 | 180 | op_r = pie.operator( MESH_OT_screenreflect.bl_idname, text='Right' ) 181 | op_r.str_dir = 'right' 182 | op_r.mode = context.scene.rmkit_props.screenreflectprops.sr_0 183 | 184 | op_d = pie.operator( MESH_OT_screenreflect.bl_idname, text='Down' ) 185 | op_d.str_dir = 'down' 186 | op_d.mode = context.scene.rmkit_props.screenreflectprops.sr_0 187 | 188 | op_u = pie.operator( MESH_OT_screenreflect.bl_idname, text='Up' ) 189 | op_u.str_dir = 'up' 190 | op_u.mode = context.scene.rmkit_props.screenreflectprops.sr_0 191 | 192 | pie.operator( 'view3d.snap_cursor_to_selected', text='Set Cursor' ) 193 | 194 | pie.operator( 'wm.call_menu_pie', text='Reflect' ).name = 'VIEW3D_MT_PIE_screenreflect_noslice' 195 | 196 | pie.operator( 'view3d.rm_zerocursor', text='Cursor to Origin' ) 197 | 198 | pie.operator( 'wm.call_menu_pie', text='Slice' ).name = 'VIEW3D_MT_PIE_screenreflect_slice' 199 | 200 | 201 | class VIEW3D_MT_PIE_screenreflect_slice( bpy.types.Menu ): 202 | """Quickly mirror the face selection about a plane whose normal is defined by a grid direction relative to viewport camera.""" 203 | bl_idname = 'VIEW3D_MT_PIE_screenreflect_slice' 204 | bl_label = 'Screen Reflect - Slice' 205 | 206 | def draw( self, context ): 207 | layout = self.layout 208 | 209 | pie = layout.menu_pie() 210 | 211 | op_l = pie.operator( MESH_OT_screenreflect.bl_idname, text='Left' ) 212 | op_l.str_dir = 'left' 213 | op_l.mode = context.scene.rmkit_props.screenreflectprops.sr_1 214 | 215 | op_r = pie.operator( MESH_OT_screenreflect.bl_idname, text='Right' ) 216 | op_r.str_dir = 'right' 217 | op_r.mode = context.scene.rmkit_props.screenreflectprops.sr_1 218 | 219 | op_d = pie.operator( MESH_OT_screenreflect.bl_idname, text='Down' ) 220 | op_d.str_dir = 'down' 221 | op_d.mode = context.scene.rmkit_props.screenreflectprops.sr_1 222 | 223 | op_u = pie.operator( MESH_OT_screenreflect.bl_idname, text='Up' ) 224 | op_u.str_dir = 'up' 225 | op_u.mode = context.scene.rmkit_props.screenreflectprops.sr_1 226 | 227 | 228 | class VIEW3D_MT_PIE_screenreflect_noslice( bpy.types.Menu ): 229 | """Quickly mirror the face selection about a plane whose normal is defined by a grid direction relative to viewport camera.""" 230 | bl_idname = 'VIEW3D_MT_PIE_screenreflect_noslice' 231 | bl_label = 'Screen Reflect - No Slice' 232 | 233 | def draw( self, context ): 234 | layout = self.layout 235 | 236 | pie = layout.menu_pie() 237 | 238 | op_l = pie.operator( MESH_OT_screenreflect.bl_idname, text='Left' ) 239 | op_l.str_dir = 'left' 240 | op_l.mode = context.scene.rmkit_props.screenreflectprops.sr_2 241 | 242 | op_r = pie.operator( MESH_OT_screenreflect.bl_idname, text='Right' ) 243 | op_r.str_dir = 'right' 244 | op_r.mode = context.scene.rmkit_props.screenreflectprops.sr_2 245 | 246 | op_d = pie.operator( MESH_OT_screenreflect.bl_idname, text='Down' ) 247 | op_d.str_dir = 'down' 248 | op_d.mode = context.scene.rmkit_props.screenreflectprops.sr_2 249 | 250 | op_u = pie.operator( MESH_OT_screenreflect.bl_idname, text='Up' ) 251 | op_u.str_dir = 'up' 252 | op_u.mode = context.scene.rmkit_props.screenreflectprops.sr_2 253 | 254 | 255 | def register(): 256 | bpy.utils.register_class( MESH_OT_screenreflect ) 257 | bpy.utils.register_class( VIEW3D_MT_PIE_screenreflect ) 258 | bpy.utils.register_class( VIEW3D_MT_PIE_screenreflect_slice ) 259 | bpy.utils.register_class( VIEW3D_MT_PIE_screenreflect_noslice ) 260 | 261 | 262 | def unregister(): 263 | bpy.utils.unregister_class( MESH_OT_screenreflect ) 264 | bpy.utils.unregister_class( VIEW3D_MT_PIE_screenreflect ) 265 | bpy.utils.unregister_class( VIEW3D_MT_PIE_screenreflect_slice ) 266 | bpy.utils.unregister_class( VIEW3D_MT_PIE_screenreflect_noslice ) -------------------------------------------------------------------------------- /selectionmode.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh, mathutils 2 | import rmlib 3 | 4 | BACKGROUND_LAYERNAME = 'rm_background' 5 | 6 | def SetSelsetMembership( bm, type, elems, layername ): 7 | if type == 'VERT': 8 | intlayers = bm.verts.layers.int 9 | selset = intlayers.get( layername, None ) 10 | for v in bm.verts: 11 | v[selset] = v in elems 12 | elif type == 'EDGE': 13 | intlayers = bm.edges.layers.int 14 | selset = intlayers.get( layername, None ) 15 | for e in bm.edges: 16 | e[selset] = e in elems 17 | elif type == 'FACE': 18 | intlayers = bm.faces.layers.int 19 | selset = intlayers.get( layername, None ) 20 | for f in bm.faces: 21 | f[selset] = f in elems 22 | else: 23 | return 24 | 25 | def GetSelsetMembership( bm, type, layername): 26 | if type == 'VERT': 27 | intlayers = bm.verts.layers.int 28 | elif type == 'EDGE': 29 | intlayers = bm.edges.layers.int 30 | elif type == 'FACE': 31 | intlayers = bm.faces.layers.int 32 | else: 33 | return [] 34 | 35 | selset = intlayers.get( layername, None ) 36 | if selset is None: 37 | return [] 38 | 39 | if type == 'VERT': 40 | return rmlib.rmVertexSet( [ v for v in bm.verts if bool( v[selset] ) ] ) 41 | elif type == 'EDGE': 42 | return rmlib.rmEdgeSet( [ e for e in bm.edges if bool( e[selset] ) ] ) 43 | else: 44 | return rmlib.rmPolygonSet( [ f for f in bm.faces if bool( f[selset] ) ] ) 45 | 46 | 47 | class MESH_OT_changetomode( bpy.types.Operator ): 48 | """Change to vert/edge/face selection mode and cache the current selection. Upon returning to previouse mode, the cached selection will be reestablished.""" 49 | bl_idname = 'mesh.rm_changemodeto' 50 | bl_label = 'Change Mode To' 51 | bl_options = { 'UNDO' } #tell blender that we support the undo/redo pannel 52 | 53 | mode_to: bpy.props.EnumProperty( 54 | items=[ ( "VERT", "Vertex", "", 1 ), 55 | ( "EDGE", "Edge", "", 2 ), 56 | ( "FACE", "Face", "", 3 ) ], 57 | name="Selection Mode", 58 | default="VERT" 59 | ) 60 | 61 | @classmethod 62 | def poll( cls, context ): 63 | return ( context.area.type == 'VIEW_3D' and 64 | context.active_object is not None and 65 | context.active_object.type == 'MESH' ) 66 | 67 | def execute( self, context ): 68 | if context.active_object.type != 'MESH': 69 | bpy.ops.object.editmode_toggle() 70 | return { 'FINISHED' } 71 | 72 | if context.mode != 'OBJECT' and not context.object.data.is_editmode: 73 | return { 'CANCELLED' } 74 | 75 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 76 | 77 | #create selsets if needed 78 | with rmmesh as rmmesh: 79 | intlayers_v = rmmesh.bmesh.verts.layers.int 80 | selset = intlayers_v.get( BACKGROUND_LAYERNAME, None ) 81 | if selset is None: 82 | selset = intlayers_v.new( BACKGROUND_LAYERNAME ) 83 | 84 | intlayers_e = rmmesh.bmesh.edges.layers.int 85 | selset = intlayers_e.get( BACKGROUND_LAYERNAME, None ) 86 | if selset is None: 87 | selset = intlayers_e.new( BACKGROUND_LAYERNAME ) 88 | 89 | intlayers_f = rmmesh.bmesh.faces.layers.int 90 | selset = intlayers_f.get( BACKGROUND_LAYERNAME, None ) 91 | if selset is None: 92 | selset = intlayers_f.new( BACKGROUND_LAYERNAME ) 93 | 94 | #exit early if we are already in the mode we are switching to 95 | sel_mode = context.tool_settings.mesh_select_mode[:] 96 | if context.mode != 'OBJECT': 97 | if ( sel_mode[0] and self.mode_to == 'VERT' ) or ( sel_mode[1] and self.mode_to == 'EDGE' ) or ( sel_mode[2] and self.mode_to == 'FACE' ): 98 | return { 'FINISHED' } 99 | 100 | #init component selset for current mode (before switching) 101 | if sel_mode[0]: 102 | with rmmesh as rmmesh: 103 | verts = rmlib.rmVertexSet.from_selection( rmmesh ) 104 | SetSelsetMembership( rmmesh.bmesh, 'VERT', verts, BACKGROUND_LAYERNAME ) 105 | elif sel_mode[1]: 106 | with rmmesh as rmmesh: 107 | edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 108 | SetSelsetMembership( rmmesh.bmesh, 'EDGE', edges, BACKGROUND_LAYERNAME ) 109 | elif sel_mode[2]: 110 | with rmmesh as rmmesh: 111 | faces = rmlib.rmPolygonSet.from_selection( rmmesh ) 112 | SetSelsetMembership( rmmesh.bmesh, 'FACE', faces, BACKGROUND_LAYERNAME ) 113 | 114 | #switch to target mode and clear selection 115 | if context.mode == 'OBJECT': 116 | bpy.ops.object.editmode_toggle() 117 | bpy.ops.mesh.select_mode( type=self.mode_to ) 118 | bpy.ops.mesh.select_all( action='DESELECT' ) 119 | 120 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 121 | 122 | #set new selection 123 | with rmmesh as rmmesh: 124 | for elem in GetSelsetMembership( rmmesh.bmesh, self.mode_to, BACKGROUND_LAYERNAME ): 125 | elem.select = True 126 | 127 | return { 'FINISHED' } 128 | 129 | 130 | class MESH_OT_convertmodeto( bpy.types.Operator ): 131 | """Convert current selection to new mode. Also caches prev selection.""" 132 | bl_idname = 'mesh.rm_convertmodeto' 133 | bl_label = 'Convert Mode To' 134 | bl_options = { 'UNDO' } #tell blender that we support the undo/redo pannel 135 | 136 | mode_to: bpy.props.EnumProperty( 137 | items=[ ( "VERT", "Vertex", "", 1 ), 138 | ( "EDGE", "Edge", "", 2 ), 139 | ( "FACE", "Face", "", 3 ) ], 140 | name="Selection Mode", 141 | default="VERT" 142 | ) 143 | 144 | @classmethod 145 | def poll( cls, context ): 146 | return ( context.area.type == 'VIEW_3D' and 147 | context.object is not None and 148 | context.object.type == 'MESH' and 149 | context.object.data.is_editmode ) 150 | 151 | def execute( self, context ): 152 | #get the selection mode 153 | if context.mode == 'OBJECT': 154 | return { 'CANCELLED' } 155 | 156 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 157 | 158 | #create selsets if needed 159 | with rmmesh as rmmesh: 160 | intlayers_v = rmmesh.bmesh.verts.layers.int 161 | selset = intlayers_v.get( BACKGROUND_LAYERNAME, None ) 162 | if selset is None: 163 | selset = intlayers_v.new( BACKGROUND_LAYERNAME ) 164 | 165 | intlayers_e = rmmesh.bmesh.edges.layers.int 166 | selset = intlayers_e.get( BACKGROUND_LAYERNAME, None ) 167 | if selset is None: 168 | selset = intlayers_e.new( BACKGROUND_LAYERNAME ) 169 | 170 | intlayers_f = rmmesh.bmesh.faces.layers.int 171 | selset = intlayers_f.get( BACKGROUND_LAYERNAME, None ) 172 | if selset is None: 173 | selset = intlayers_f.new( BACKGROUND_LAYERNAME ) 174 | 175 | #exit early if we are already in the mode we are converting to 176 | sel_mode = context.tool_settings.mesh_select_mode[:] 177 | if ( sel_mode[0] and self.mode_to == 'VERT' ) or ( sel_mode[1] and self.mode_to == 'EDGE' ) or ( sel_mode[2] and self.mode_to == 'FACE' ): 178 | return { 'FINISHED' } 179 | 180 | #init component selset for current mode (before converting) 181 | if sel_mode[0]: 182 | with rmmesh as rmmesh: 183 | verts = rmlib.rmVertexSet.from_selection( rmmesh ) 184 | if self.mode_to == 'EDGE': 185 | SetSelsetMembership( rmmesh.bmesh, self.mode_to, verts.edges, BACKGROUND_LAYERNAME ) 186 | elif self.mode_to == 'FACE': 187 | SetSelsetMembership( rmmesh.bmesh, self.mode_to, verts.polygons, BACKGROUND_LAYERNAME ) 188 | 189 | elif sel_mode[1]: 190 | with rmmesh as rmmesh: 191 | edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 192 | 193 | sel_faces = set() 194 | if self.mode_to == 'FACE': 195 | for v in edges.vertices: 196 | sel_count = 0 197 | is_open = False 198 | selected_boundary = False 199 | for e in v.link_edges: 200 | if e.is_boundary: 201 | if e.select: 202 | selected_boundary = True 203 | is_open = True 204 | if e.select: 205 | sel_count += 1 206 | if ( is_open and not selected_boundary ) or sel_count > 1: 207 | for f in v.link_faces: 208 | sel_faces.add( f ) 209 | 210 | if self.mode_to == 'VERT': 211 | SetSelsetMembership( rmmesh.bmesh, self.mode_to, edges.vertices, BACKGROUND_LAYERNAME ) 212 | elif self.mode_to == 'FACE': 213 | faces = set( edges.polygons ).union( sel_faces ) 214 | SetSelsetMembership( rmmesh.bmesh, self.mode_to, faces, BACKGROUND_LAYERNAME ) 215 | 216 | elif sel_mode[2]: 217 | with rmmesh as rmmesh: 218 | faces = rmlib.rmPolygonSet.from_selection( rmmesh ) 219 | if self.mode_to == 'VERT': 220 | SetSelsetMembership( rmmesh.bmesh, self.mode_to, faces.vertices, BACKGROUND_LAYERNAME ) 221 | elif self.mode_to == 'EDGE': 222 | SetSelsetMembership( rmmesh.bmesh, self.mode_to, faces.edges, BACKGROUND_LAYERNAME ) 223 | 224 | #change mode and clear component selection 225 | bpy.ops.mesh.select_mode( type=self.mode_to ) 226 | bpy.ops.mesh.select_all( action='DESELECT' ) 227 | 228 | #select result 229 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 230 | with rmmesh as rmmesh: 231 | rmmesh.readonly = True 232 | for elem in GetSelsetMembership( rmmesh.bmesh, self.mode_to, BACKGROUND_LAYERNAME ): 233 | elem.select = True 234 | 235 | return { 'FINISHED' } 236 | 237 | 238 | class MESH_OT_continuous( bpy.types.Operator ): 239 | """Extend current element selection by 3d continuity.""" 240 | bl_idname = 'mesh.rm_continuous' 241 | bl_label = 'Select Continuous' 242 | bl_options = { 'UNDO' } 243 | 244 | mode: bpy.props.EnumProperty( 245 | name='Mode', 246 | description='Set/Add/Remove to/from selection.', 247 | items=[ ( "set", "Set", "", 1 ), 248 | ( "add", "Add", "", 2 ), 249 | ( "remove", "Remove", "", 3 ) ], 250 | default='set' 251 | ) 252 | 253 | def __init__( self, *args, **kwargs ): 254 | if bpy.app.version >= ( 4, 4, 0 ): 255 | super().__init__( *args, **kwargs ) 256 | 257 | self.mos_elem = None 258 | 259 | @classmethod 260 | def poll( cls, context ): 261 | return ( context.area.type == 'VIEW_3D' and 262 | len( context.editable_objects ) > 0 and 263 | context.mode == 'EDIT_MESH' ) 264 | 265 | def invoke( self, context, event ): 266 | mouse_pos = mathutils.Vector( ( event.mouse_region_x, event.mouse_region_y ) ) 267 | 268 | me = context.active_object.data 269 | bm = bmesh.from_edit_mesh( me ) 270 | rmmesh = rmlib.rmMesh.from_bmesh( context.active_object, bm ) 271 | 272 | sel_mode = context.tool_settings.mesh_select_mode[:] 273 | if sel_mode[0]: 274 | try: 275 | self.mos_elem = rmlib.rmVertexSet.from_mos( rmmesh, context, mouse_pos )[0] 276 | except: 277 | self.report( { 'INFO' }, 'No mos vert found.' ) 278 | elif sel_mode[1]: 279 | try: 280 | self.mos_elem = rmlib.rmEdgeSet.from_mos( rmmesh, context, mouse_pos )[0] 281 | except: 282 | self.report( { 'INFO' }, 'No mos edge found.' ) 283 | else: 284 | try: 285 | self.mos_elem = rmlib.rmPolygonSet.from_mos( rmmesh, context, mouse_pos )[0] 286 | except: 287 | self.report( { 'INFO' }, 'No mos face found.' ) 288 | 289 | rmlib.clear_tags( rmmesh.bmesh ) 290 | 291 | sel_mode = context.tool_settings.mesh_select_mode[:] 292 | if sel_mode[0]: 293 | if self.mos_elem is None: 294 | selected_verts = rmlib.rmVertexSet.from_selection( rmmesh ) 295 | else: 296 | selected_verts = rmlib.rmVertexSet( [ self.mos_elem ] ) 297 | rmmesh.bmesh.select_history.add( self.mos_elem ) 298 | if self.mode == 'set': 299 | for v in rmmesh.bmesh.verts: 300 | v.select_set( False ) 301 | for g in selected_verts.group( element=True ): 302 | for v in g: 303 | v.select_set( self.mode != 'remove' ) 304 | rmmesh.bmesh.select_flush_mode() 305 | 306 | elif sel_mode[1]: 307 | if self.mos_elem is not None: 308 | self.mos_elem.select_set( True ) 309 | rmmesh.bmesh.select_history.add( self.mos_elem ) 310 | rmmesh.bmesh.select_flush_mode() 311 | bpy.ops.mesh.rm_loop( force_boundary=True, mode=self.mode, evaluate_all_selected=False ) 312 | else: 313 | selected_edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 314 | if len( selected_edges ) > 0: 315 | bpy.ops.mesh.rm_loop( force_boundary=True, mode=self.mode, evaluate_all_selected=True ) 316 | 317 | else: 318 | if self.mos_elem is None: 319 | selected_faces = rmlib.rmPolygonSet.from_selection( rmmesh ) 320 | else: 321 | selected_faces = rmlib.rmPolygonSet( [ self.mos_elem ] ) 322 | rmmesh.bmesh.select_history.add( self.mos_elem ) 323 | if self.mode == 'set': 324 | for f in rmmesh.bmesh.faces: 325 | f.select_set( False ) 326 | for g in selected_faces.group( element=True ): 327 | for f in g: 328 | f.select_set( self.mode != 'remove' ) 329 | rmmesh.bmesh.select_flush_mode() 330 | 331 | rmlib.clear_tags( rmmesh.bmesh ) 332 | 333 | bmesh.update_edit_mesh( me, loop_triangles=False, destructive=False ) 334 | bm.free() 335 | del bm 336 | 337 | return { 'FINISHED' } 338 | 339 | 340 | class MESH_OT_invertcontinuous( bpy.types.Operator ): 341 | """Invert the selection on elements that are 3d continuouse with the current selection.""" 342 | bl_idname = 'mesh.rm_invertcontinuous' 343 | bl_label = 'Invert Continuous' 344 | bl_options = { 'UNDO' } 345 | 346 | @classmethod 347 | def poll( cls, context ): 348 | return ( context.area.type == 'VIEW_3D' and 349 | len( context.editable_objects ) > 0 and 350 | context.mode == 'EDIT_MESH' ) 351 | 352 | def execute( self, context ): 353 | #get the selection mode 354 | if context.object is None or context.mode == 'OBJECT': 355 | return { 'CANCELLED' } 356 | 357 | for rmmesh in rmlib.iter_edit_meshes( context, mode_filter=True ): 358 | with rmmesh as rmmesh: 359 | sel_mode = context.tool_settings.mesh_select_mode[:] 360 | 361 | if sel_mode[0]: 362 | verts = rmlib.rmVertexSet.from_selection( rmmesh ) 363 | allverts = rmlib.rmVertexSet() 364 | for g in verts.group( element=True ): 365 | allverts += g 366 | for v in allverts: 367 | v.select = v not in verts 368 | 369 | 370 | elif sel_mode[1]: 371 | edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 372 | alledges = rmlib.rmEdgeSet() 373 | for g in edges.group( element=True ): 374 | alledges += g 375 | for e in alledges: 376 | e.select = e not in edges 377 | 378 | elif sel_mode[2]: 379 | faces = rmlib.rmPolygonSet.from_selection( rmmesh ) 380 | allfaces = rmlib.rmPolygonSet() 381 | for g in faces.group( element=True ): 382 | allfaces += g 383 | for f in allfaces: 384 | f.select = f not in faces 385 | 386 | return { 'FINISHED' } 387 | 388 | 389 | def register(): 390 | bpy.utils.register_class( MESH_OT_changetomode ) 391 | bpy.utils.register_class( MESH_OT_convertmodeto ) 392 | bpy.utils.register_class( MESH_OT_invertcontinuous ) 393 | bpy.utils.register_class( MESH_OT_continuous ) 394 | 395 | 396 | def unregister(): 397 | bpy.utils.unregister_class( MESH_OT_changetomode ) 398 | bpy.utils.unregister_class( MESH_OT_convertmodeto ) 399 | bpy.utils.unregister_class( MESH_OT_invertcontinuous ) 400 | bpy.utils.unregister_class( MESH_OT_continuous ) -------------------------------------------------------------------------------- /targetweld.py: -------------------------------------------------------------------------------- 1 | import bpy, bmesh 2 | import rmlib 3 | 4 | def chain_is_sorted( chain ): 5 | e = rmlib.rmEdgeSet.from_endpoints( chain[0], chain[1] ) 6 | verts = list( e.link_faces[0].verts ) 7 | idx = verts.index( chain[1] ) 8 | return verts[ idx - 1 ] == chain[0] 9 | 10 | 11 | def chain_is_boundary( chain ): 12 | for i in range( 1, len( chain ) ): 13 | e = rmlib.rmEdgeSet.from_endpoints( chain[i-1], chain[i] ) 14 | if not e.is_boundary: 15 | return False 16 | return True 17 | 18 | 19 | class MESH_OT_targetweld( bpy.types.Operator ): 20 | """Target weld verts or edge loops to active vert/edge.""" 21 | bl_idname = 'mesh.rm_targetweld' 22 | bl_label = 'Target Weld' 23 | bl_options = { 'UNDO' } 24 | 25 | @classmethod 26 | def poll( cls, context ): 27 | return ( context.area.type == 'VIEW_3D' and 28 | context.active_object is not None and 29 | context.active_object.type == 'MESH' and 30 | context.object.data.is_editmode ) 31 | 32 | def execute( self, context ): 33 | if context.object is None or context.mode == 'OBJECT': 34 | return { 'CANCELLED' } 35 | 36 | if context.object.type != 'MESH': 37 | return { 'CANCELLED' } 38 | 39 | sel_mode = context.tool_settings.mesh_select_mode[:] 40 | if sel_mode[2]: 41 | return { 'CANCELLED' } 42 | 43 | rmmesh = rmlib.rmMesh.GetActive( context ) 44 | with rmmesh as rmmesh: 45 | if sel_mode[0]: 46 | verts = rmlib.rmVertexSet.from_selection( rmmesh ) 47 | active_vert = rmmesh.bmesh.select_history.active 48 | if not isinstance( active_vert, bmesh.types.BMVert ): 49 | active_vert = verts[0] 50 | if len( verts ) < 2: 51 | return { 'CANCELLED' } 52 | target_vert = verts.pop( verts.index( active_vert ) ) 53 | for v in verts: 54 | v.co = target_vert.co 55 | verts.append( active_vert ) 56 | bmesh.ops.remove_doubles( rmmesh.bmesh, verts=verts, dist=0.00001 ) 57 | 58 | elif sel_mode[1]: 59 | #get verts of current active edge 60 | edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 61 | if len( edges ) < 2: 62 | return { 'CANCELLED' } 63 | for v in edges.vertices: 64 | v.tag = False 65 | active_edge = rmmesh.bmesh.select_history.active 66 | if active_edge is None or not isinstance( active_edge, bmesh.types.BMEdge ): 67 | active_edge = edges[0] 68 | active_verts = list( active_edge.verts ) 69 | 70 | #break up edges into chains and set the one that includes the active verts to be the weld target 71 | chains = edges.vert_chain() 72 | if len( chains ) < 2: 73 | return { 'CANCELLED' } 74 | for i, chain in enumerate( chains ): 75 | if active_verts[0] in chain and active_verts[1] in chain: 76 | break 77 | target_chain = chains.pop( i ) 78 | for v in target_chain: 79 | v.tag = True 80 | 81 | #weld open edges 82 | if chain_is_boundary( target_chain ): 83 | target_chain_is_sorted = chain_is_sorted( target_chain ) 84 | for i, chain in enumerate( chains ): 85 | if not chain_is_boundary( chain ): 86 | continue 87 | if target_chain_is_sorted and chain_is_sorted( chain ): 88 | chain.reverse() 89 | for j in range( len( target_chain ) ): 90 | try: 91 | chain[j].co = target_chain[j].co 92 | chain[j].tag = True #tag so we can target weld to these to these later 93 | except IndexError: 94 | break 95 | 96 | #weld closed edges 97 | verts_welded = True 98 | skip_idxs = set() 99 | while( verts_welded ): 100 | verts_welded = False 101 | for i, chain in enumerate( chains ): 102 | if i in skip_idxs: 103 | continue 104 | for v in chain: 105 | if v.tag: 106 | continue 107 | for e in v.link_edges: 108 | l_v = e.other_vert( v ) 109 | if l_v.tag: 110 | v.co = l_v.co 111 | verts_welded = True 112 | skip_idxs.add( i ) 113 | break 114 | for v in chain: 115 | v.tag = True 116 | 117 | bmesh.ops.remove_doubles( rmmesh.bmesh, verts=edges.vertices, dist=0.0001 ) 118 | 119 | 120 | return { 'FINISHED' } 121 | 122 | 123 | def register(): 124 | bpy.utils.register_class( MESH_OT_targetweld ) 125 | 126 | def unregister(): 127 | bpy.utils.unregister_class( MESH_OT_targetweld ) -------------------------------------------------------------------------------- /thicken.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import rmlib 4 | import mathutils 5 | 6 | def BridgeSurfaces( bm, faces1, faces2 ): 7 | new_faces = rmlib.rmPolygonSet( [] ) 8 | 9 | #create edge loops from two poly surfaces 10 | loops_a = rmlib.rmEdgeSet( [ e for e in faces1.edges if e.is_boundary ] ).vert_chain() 11 | loops_b = rmlib.rmEdgeSet( [ e for e in faces2.edges if e.is_boundary ] ).vert_chain() 12 | 13 | for l_a in loops_a: 14 | p1 = mathutils.Vector( l_a[0].co.copy() ) 15 | nearest_loop = None 16 | nearest_dist = 99999.9 17 | for l_b in loops_b: 18 | for v in l_b: 19 | p2 = mathutils.Vector( v.co.copy() ) 20 | dist = ( p2 - p1 ).length 21 | if dist < nearest_dist: 22 | nearest_dist = dist 23 | nearest_loop = l_b 24 | if nearest_loop is None: 25 | continue 26 | 27 | loops = [ l_a, nearest_loop ] 28 | 29 | #check if loop1 is sorted based of a neigh face 30 | for i in range( 2 ): 31 | epts = ( loops[i][0], loops[i][1] ) 32 | edge = rmlib.rmEdgeSet.from_endpoints( epts[0], epts[1] ) 33 | face_verts = list( edge.link_faces[0].verts ) 34 | vert_idx = face_verts.index( epts[1] ) 35 | if face_verts[vert_idx-1] == epts[0]: 36 | loops[i].reverse() 37 | 38 | #align loop1 and loop2 39 | sample_pos = loops[0][0].co.copy() 40 | min_len = 9999999.9 41 | min_idx = -1 42 | for i, v in enumerate( loops[1] ): 43 | d = ( v.co - sample_pos ).length 44 | if d < min_len: 45 | min_len = d 46 | min_idx = i 47 | 48 | #bridge the loops 49 | vcount = len( loops[0] ) 50 | for i in range( vcount ): 51 | j = ( min_idx + i ) % vcount 52 | quad = ( loops[0][i-1], loops[0][i], loops[1][j], loops[1][j-1] ) 53 | new_faces.append( bm.faces.new( quad, faces1[0] ) ) 54 | 55 | return new_faces 56 | 57 | 58 | class MESH_OT_thicken( bpy.types.Operator ): 59 | """Same as solidify, just with better controls.""" 60 | bl_idname = 'mesh.rm_thicken' 61 | bl_label = 'Thicken' 62 | bl_options = { 'REGISTER', 'UNDO' } 63 | 64 | thickness: bpy.props.FloatProperty( 65 | name='Thickenss', 66 | default=1.0 67 | ) 68 | 69 | center: bpy.props.BoolProperty( 70 | name='From Center', 71 | default=False 72 | ) 73 | 74 | def cancel( self, context ): 75 | if hasattr( self, 'bmesh' ): 76 | if self.bmesh is not None: 77 | self.bmesh.free() 78 | 79 | @classmethod 80 | def poll( cls, context ): 81 | return ( context.area.type == 'VIEW_3D' and 82 | context.active_object is not None and 83 | context.active_object.type == 'MESH' and 84 | context.object.data.is_editmode ) 85 | 86 | def execute( self, context ): 87 | bpy.ops.object.mode_set( mode='OBJECT', toggle=False ) 88 | 89 | bm = self.bmesh.copy() 90 | 91 | polys = rmlib.rmPolygonSet( [ f for f in bm.faces if f.select ] ) 92 | for g in polys.group(): 93 | geom1 = bmesh.ops.duplicate( bm, geom=g ) 94 | faces1 = rmlib.rmPolygonSet() 95 | for elem in geom1['geom']: 96 | if isinstance( elem, bmesh.types.BMFace ): 97 | faces1.append( elem ) 98 | 99 | geom2 = bmesh.ops.duplicate( bm, geom=g ) 100 | faces2 = rmlib.rmPolygonSet() 101 | for elem in geom2['geom']: 102 | if isinstance( elem, bmesh.types.BMFace ): 103 | faces2.append( elem ) 104 | 105 | bm.verts.ensure_lookup_table() 106 | 107 | #bridget the two surfaces 108 | bridge_faces = BridgeSurfaces( bm, faces1, faces2 ) 109 | 110 | #offset verts 111 | if self.center: 112 | for v in faces1.vertices: 113 | v.co += mathutils.Vector( v.normal ) * abs( self.thickness ) * 0.5 114 | for v in faces2.vertices: 115 | v.co -= mathutils.Vector( v.normal ) * abs( self.thickness ) * 0.5 116 | else: 117 | for v in faces1.vertices: 118 | v.co += mathutils.Vector( v.normal ) * self.thickness 119 | 120 | #flip faces2 121 | bmesh.ops.reverse_faces( bm, faces=faces2, flip_multires=True ) 122 | if self.thickness < 0.0: 123 | bmesh.ops.reverse_faces( bm, faces=( bridge_faces + faces1 + faces2 ), flip_multires=True ) 124 | 125 | #delete original geo 126 | bmesh.ops.delete( bm, geom=polys, context='FACES' ) 127 | 128 | targetMesh = context.active_object.data 129 | bm.to_mesh( targetMesh ) 130 | bm.calc_loop_triangles() 131 | targetMesh.update() 132 | bm.free() 133 | 134 | bpy.ops.object.mode_set( mode='EDIT', toggle=False ) 135 | 136 | return { 'FINISHED' } 137 | 138 | def modal( self, context, event ): 139 | if event.type == 'LEFTMOUSE': 140 | return { 'FINISHED' } 141 | elif event.type == 'MOUSEMOVE': 142 | delta_x = float( event.mouse_x - event.mouse_prev_press_x ) / context.region.width 143 | if delta_x != self.prev_delta: 144 | self.prev_delta = delta_x 145 | self.thickness = delta_x * 4.0 146 | self.execute( context ) 147 | elif event.type == 'ESC': 148 | return { 'CANCELLED' } 149 | 150 | return { 'RUNNING_MODAL' } 151 | 152 | def invoke( self, context, event ): 153 | self.bmesh = None 154 | self.prev_delta = 0 155 | 156 | if context.object is None or context.mode == 'OBJECT': 157 | return { 'CANCELLED' } 158 | 159 | if context.object.type != 'MESH': 160 | return { 'CANCELLED' } 161 | 162 | sel_mode = context.tool_settings.mesh_select_mode[:] 163 | if not sel_mode[2]: 164 | return { 'CANCELLED' } 165 | 166 | rmmesh = rmlib.rmMesh.GetActive( context ) 167 | if rmmesh is not None: 168 | with rmmesh as rmmesh: 169 | rmmesh.readme = True 170 | self.bmesh = rmmesh.bmesh.copy() 171 | 172 | context.window_manager.modal_handler_add( self ) 173 | return { 'RUNNING_MODAL' } 174 | 175 | 176 | def register(): 177 | bpy.utils.register_class( MESH_OT_thicken ) 178 | 179 | def unregister(): 180 | bpy.utils.unregister_class( MESH_OT_thicken ) -------------------------------------------------------------------------------- /workplane.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import gpu 3 | from gpu_extras.batch import batch_for_shader 4 | from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_location_3d 5 | import math, time 6 | import mathutils 7 | import rmlib 8 | 9 | GRID_RENDER = None 10 | 11 | 12 | class GridRenderManager: 13 | coords = [] 14 | colors = [] 15 | shader = None 16 | batch = None 17 | handle = None 18 | active = False 19 | matrix = mathutils.Matrix.Identity( 4 ) 20 | scale = 1.0 21 | 22 | def __init__( self, context ): 23 | GridRenderManager.shader = gpu.shader.from_builtin( 'POLYLINE_SMOOTH_COLOR' ) 24 | self.update_scale( context ) 25 | 26 | def update_scale( self, context ): 27 | if context.area is None: 28 | return 29 | 30 | width = context.region.width 31 | height = context.region.height 32 | top = ( width * 0.5, height ) 33 | left = ( 0.0, height * 0.5 ) 34 | right = ( width, height * 0.5 ) 35 | bottom = ( width * 0.5, 0.0 ) 36 | center = ( width * 0.5, height * 0.5 ) 37 | screen_pts = ( top, left, right, bottom, center ) 38 | 39 | hit_list = [] 40 | pp = GridRenderManager.matrix.to_translation() 41 | pn = GridRenderManager.matrix.to_3x3().col[2] 42 | for pt in screen_pts: 43 | dir = region_2d_to_vector_3d( context.region, context.region_data, pt ) 44 | pos = region_2d_to_location_3d( context.region, context.region_data, pt, dir ) 45 | a = pos 46 | b = a + ( dir * 1000.0 ) 47 | a = a - ( dir * 1000.0 ) 48 | hit_pos = mathutils.geometry.intersect_line_plane( a, b, pp, pn ) 49 | if hit_pos is None: 50 | continue 51 | hit_list.append( hit_pos ) 52 | 53 | min_dist = 500 54 | for p in hit_list[:-1]: 55 | d = ( p - hit_list[-1] ).length 56 | min_dist = min( d, min_dist ) 57 | 58 | scale_idx = math.floor( math.log2( min_dist ) ) 59 | GridRenderManager.scale = math.pow( 2, scale_idx - 2 ) 60 | 61 | self.shader_batch() 62 | 63 | def shader_batch( self ): 64 | GridRenderManager.coords.clear() 65 | GridRenderManager.colors.clear() 66 | n = 1.0 67 | s = GridRenderManager.scale 68 | 69 | for i in range( -10, 10 + 1 ): 70 | GridRenderManager.coords.append( ( n * i * s, -10.0 * s, 0.0 ) ) 71 | GridRenderManager.coords.append( ( n * i * s, 10.0 * s, 0.0 ) ) 72 | GridRenderManager.coords.append( ( -10.0 * s, n * i * s, 0.0 ) ) 73 | GridRenderManager.coords.append( ( 10.0 * s, n * i * s, 0.0 ) ) 74 | 75 | if i == 0: 76 | GridRenderManager.colors.append( ( 1.0, 0.0, 0.0, 0.5 ) ) 77 | GridRenderManager.colors.append( ( 1.0, 0.0, 0.0, 0.5 ) ) 78 | GridRenderManager.colors.append( ( 0.0, 1.0, 0.0, 0.5 ) ) 79 | GridRenderManager.colors.append( ( 0.0, 1.0, 0.0, 0.5 ) ) 80 | else: 81 | GridRenderManager.colors.append( ( 0.5, 0.5, 0.5, 0.5 ) ) 82 | GridRenderManager.colors.append( ( 0.5, 0.5, 0.5, 0.5 ) ) 83 | GridRenderManager.colors.append( ( 0.5, 0.5, 0.5, 0.5 ) ) 84 | GridRenderManager.colors.append( ( 0.5, 0.5, 0.5, 0.5 ) ) 85 | 86 | content = { 'pos' : GridRenderManager.coords, 'color' : GridRenderManager.colors } 87 | GridRenderManager.batch = batch_for_shader( GridRenderManager.shader, 'LINES', content ) 88 | 89 | def draw( self ): 90 | if GridRenderManager.batch: 91 | gpu.matrix.push() 92 | gpu.matrix.load_matrix( GridRenderManager.matrix ) 93 | gpu.matrix.push_projection() 94 | gpu.matrix.load_projection_matrix( bpy.context.region_data.perspective_matrix ) 95 | 96 | GridRenderManager.shader.bind() 97 | GridRenderManager.shader.uniform_float( 'lineWidth', 1 ) 98 | region = bpy.context.region 99 | GridRenderManager.shader.uniform_float( 'viewportSize', ( region.width, region.height ) ) 100 | 101 | GridRenderManager.batch.draw( GridRenderManager.shader ) 102 | 103 | gpu.matrix.pop() 104 | gpu.matrix.pop_projection() 105 | 106 | def doDraw( self ): 107 | GridRenderManager.handle = bpy.types.SpaceView3D.draw_handler_add( self.draw, (), 'WINDOW', 'POST_VIEW' ) 108 | GridRenderManager.active = True 109 | 110 | def stopDraw( self, context ): 111 | bpy.types.SpaceView3D.draw_handler_remove( GridRenderManager.handle, 'WINDOW' ) 112 | GridRenderManager.active = False 113 | 114 | for window in context.window_manager.windows: 115 | for area in window.screen.areas: 116 | if area.type == 'VIEW_3D': 117 | for region in area.regions: 118 | if region.type == 'WINDOW': 119 | region.tag_redraw() 120 | 121 | 122 | class MESH_OT_workplane( bpy.types.Operator ): 123 | """Toggle the workplane. Workplane orientation is determined by vert/edge/face selection.""" 124 | bl_idname = 'view3d.rm_workplane' 125 | bl_label = 'Workplane' 126 | 127 | @classmethod 128 | def poll( cls, context ): 129 | return context.area.type == 'VIEW_3D' 130 | 131 | def invoke(self, context, event): 132 | #add a timer to modal 133 | wm = context.window_manager 134 | self._timer = wm.event_timer_add(1.0/8.0, window=context.window) 135 | wm.modal_handler_add(self) 136 | 137 | self.execute( context ) 138 | 139 | return {'RUNNING_MODAL'} 140 | 141 | def modal(self, context, event): 142 | global GRID_RENDER 143 | 144 | #kill modal if inactive 145 | if not GRID_RENDER.active: 146 | return {'FINISHED'} 147 | 148 | #check if user manually left workplane transform orientation mode 149 | if event.type == 'TIMER': 150 | GRID_RENDER.update_scale( context ) 151 | 152 | if not context.scene.transform_orientation_slots[0].type == 'WORKPLANE': 153 | GRID_RENDER.stopDraw( context ) 154 | bpy.context.space_data.overlay.show_floor = True 155 | bpy.context.space_data.overlay.show_axis_x = True 156 | bpy.context.space_data.overlay.show_axis_y = True 157 | 158 | selected_type = context.scene.transform_orientation_slots[0].type 159 | try: 160 | context.scene.transform_orientation_slots[0].type = 'WORKPLANE' 161 | bpy.ops.transform.delete_orientation() 162 | context.scene.transform_orientation_slots[0].type = selected_type 163 | except TypeError: 164 | pass 165 | 166 | return {'FINISHED'} 167 | 168 | return {"PASS_THROUGH"} 169 | 170 | def execute(self, context): 171 | global GRID_RENDER 172 | if GRID_RENDER is None: 173 | GRID_RENDER = GridRenderManager( context ) 174 | 175 | if not GridRenderManager.active and context.mode == 'EDIT_MESH': 176 | sel_mode = context.tool_settings.mesh_select_mode[:] 177 | rmmesh = rmlib.rmMesh.GetActive( context ) 178 | with rmmesh as rmmesh: 179 | rmmesh.readonly = True 180 | if sel_mode[0]: 181 | sel_verts = rmlib.rmVertexSet.from_selection( rmmesh ) 182 | if len( sel_verts ) > 0: 183 | v = sel_verts[0] 184 | 185 | v_n = v.normal 186 | 187 | v_t = mathutils.Vector( ( 0.0, 0.0001, 1.0 ) ) 188 | for e in v.link_edges: 189 | v1, v2 = e.verts 190 | v_t = v2.co - v1.co 191 | v_t = v_n.cross( v_t.normalized() ) 192 | 193 | GridRenderManager.matrix = rmmesh.world_transform @ rmlib.util.LookAt( v_n, v_t, v.co ) 194 | 195 | elif sel_mode[1]: 196 | sel_edges = rmlib.rmEdgeSet.from_selection( rmmesh ) 197 | if len( sel_edges ) > 0: 198 | e = sel_edges[0] 199 | 200 | e_n = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 201 | for p in e.link_faces: 202 | e_n += p.normal 203 | if e_n.length < 0.00000001: 204 | mathutils.Vector( ( 0.0, 0.0001, 1.0 ) ) 205 | e_n.normalize() 206 | 207 | v1, v2 = e.verts 208 | e_t = v2.co - v1.co 209 | e_t = e_n.cross( e_t.normalized() ) 210 | 211 | e_p = ( v1.co + v2.co ) * 0.5 212 | 213 | GridRenderManager.matrix = rmmesh.world_transform @ rmlib.util.LookAt( e_n, e_t, e_p ) 214 | 215 | elif sel_mode[2]: 216 | sel_polys = rmlib.rmPolygonSet.from_selection( rmmesh ) 217 | if len( sel_polys ) > 0: 218 | p = sel_polys[0] 219 | 220 | GridRenderManager.matrix = rmmesh.world_transform @ rmlib.util.LookAt( p.normal, p.calc_tangent_edge_pair(), p.calc_center_median() ) 221 | 222 | elif not GridRenderManager.active and context.mode == 'OBJECT' and context.object is not None: 223 | GridRenderManager.matrix = mathutils.Matrix( context.object.matrix_world ) 224 | 225 | #toggle the render state of the GRID_RENDER global 226 | if GridRenderManager.active: 227 | bpy.context.space_data.overlay.show_floor = bpy.context.scene.rmkit_props.workplaneprops['prop_show_floor'] 228 | bpy.context.space_data.overlay.show_axis_x = bpy.context.scene.rmkit_props.workplaneprops['prop_show_x'] 229 | bpy.context.space_data.overlay.show_axis_y = bpy.context.scene.rmkit_props.workplaneprops['prop_show_y'] 230 | bpy.context.space_data.overlay.show_axis_z = bpy.context.scene.rmkit_props.workplaneprops['prop_show_z'] 231 | 232 | context.scene.transform_orientation_slots[0].type = 'GLOBAL' 233 | GRID_RENDER.stopDraw( context ) 234 | else: 235 | bpy.context.scene.rmkit_props.workplaneprops['prop_show_floor'] = bpy.context.space_data.overlay.show_floor 236 | bpy.context.scene.rmkit_props.workplaneprops['prop_show_x'] = bpy.context.space_data.overlay.show_axis_x 237 | bpy.context.scene.rmkit_props.workplaneprops['prop_show_y'] = bpy.context.space_data.overlay.show_axis_y 238 | bpy.context.scene.rmkit_props.workplaneprops['prop_show_z'] = bpy.context.space_data.overlay.show_axis_z 239 | 240 | bpy.context.space_data.overlay.show_floor = False 241 | bpy.context.space_data.overlay.show_axis_x = False 242 | bpy.context.space_data.overlay.show_axis_y = False 243 | bpy.context.space_data.overlay.show_axis_z = False 244 | 245 | bpy.ops.transform.create_orientation( name='WORKPLANE', use=True, use_view=True, overwrite=True ) 246 | orientation = context.scene.transform_orientation_slots[0].custom_orientation 247 | orientation.matrix = GRID_RENDER.matrix.to_3x3() 248 | 249 | GRID_RENDER.doDraw() 250 | 251 | return { 'FINISHED' } 252 | 253 | 254 | class MESH_OT_togglegrid( bpy.types.Operator ): 255 | """Toggle view3d grid.""" 256 | bl_idname = 'view3d.rm_togglegrid' 257 | bl_label = 'Toggle Grid' 258 | bl_options = { 'UNDO' } 259 | 260 | @classmethod 261 | def poll( cls, context ): 262 | #used by blender to test if operator can show up in a menu or as a button in the UI 263 | return ( context.area.type == 'VIEW_3D' ) 264 | 265 | def execute( self, context ): 266 | b1 = bpy.context.space_data.overlay.show_floor 267 | b2 = bpy.context.space_data.overlay.show_axis_x 268 | b3 = bpy.context.space_data.overlay.show_axis_y 269 | b4 = bpy.context.space_data.overlay.show_axis_z 270 | 271 | if b1 or b2 or b3: 272 | bpy.context.scene.rmkit_props.workplaneprops['prop_show_floor'] = b1 273 | bpy.context.scene.rmkit_props.workplaneprops['prop_show_x'] = b2 274 | bpy.context.scene.rmkit_props.workplaneprops['prop_show_y'] = b3 275 | bpy.context.scene.rmkit_props.workplaneprops['prop_show_z'] = b4 276 | bpy.context.space_data.overlay.show_floor = False 277 | bpy.context.space_data.overlay.show_axis_x = False 278 | bpy.context.space_data.overlay.show_axis_y = False 279 | bpy.context.space_data.overlay.show_axis_z = False 280 | else: 281 | bpy.context.space_data.overlay.show_floor = bpy.context.scene.rmkit_props.workplaneprops['prop_show_floor'] 282 | bpy.context.space_data.overlay.show_axis_x = bpy.context.scene.rmkit_props.workplaneprops['prop_show_x'] 283 | bpy.context.space_data.overlay.show_axis_y = bpy.context.scene.rmkit_props.workplaneprops['prop_show_y'] 284 | bpy.context.space_data.overlay.show_axis_z = bpy.context.scene.rmkit_props.workplaneprops['prop_show_z'] 285 | bpy.context.space_data.overlay.show_floor = True 286 | bpy.context.space_data.overlay.show_axis_x = True 287 | bpy.context.space_data.overlay.show_axis_y = True 288 | bpy.context.space_data.overlay.show_axis_z = False 289 | 290 | return { 'FINISHED' } 291 | 292 | 293 | class MESH_OT_zerocursor( bpy.types.Operator ): 294 | """Move Cursor to grid origin.""" 295 | bl_idname = 'view3d.rm_zerocursor' 296 | bl_label = 'Cursor to Origin' 297 | bl_options = { 'UNDO' } 298 | 299 | @classmethod 300 | def poll( cls, context ): 301 | return ( context.area.type == 'VIEW_3D' ) 302 | 303 | def execute( self, context ): 304 | global GRID_RENDER 305 | if GRID_RENDER is None or not GRID_RENDER.active: 306 | context.scene.cursor.location = mathutils.Vector( ( 0.0, 0.0, 0.0 ) ) 307 | else: 308 | context.scene.cursor.location = GRID_RENDER.matrix.to_translation() 309 | context.scene.cursor.matrix = GRID_RENDER.matrix 310 | 311 | return { 'FINISHED' } 312 | 313 | 314 | def register(): 315 | bpy.utils.register_class( MESH_OT_workplane ) 316 | bpy.utils.register_class( MESH_OT_togglegrid ) 317 | bpy.utils.register_class( MESH_OT_zerocursor ) 318 | 319 | 320 | def unregister(): 321 | bpy.utils.unregister_class( MESH_OT_workplane ) 322 | bpy.utils.unregister_class( MESH_OT_togglegrid ) 323 | bpy.utils.unregister_class( MESH_OT_zerocursor ) --------------------------------------------------------------------------------