New Object From Selected Geometry Blender Add-on #16
266
New object from selected.py
Normal file
266
New object from selected.py
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# ***** END GPL LICENSE BLOCK *****
|
||||||
|
|
||||||
|
bl_info = {
|
||||||
|
"name": "New Object From Selected Geometry",
|
||||||
|
"description": "Creates a new object from the selected geometry in Edit mode",
|
||||||
|
"author": "Giambattista Caltabiano",
|
||||||
|
"version": (1, 0),
|
||||||
|
"blender": (3, 5, 0),
|
||||||
|
"location": "View3D > Mesh",
|
||||||
|
"warning": "",
|
||||||
|
"doc_url": "https://projects.blender.org/Giambattista-Caltabiano/NewObjectFromSelectedGeometryAdd-on.git",
|
||||||
|
"tracker_url": "https://projects.blender.org/Giambattista-Caltabiano/NewObjectFromSelectedGeometryAdd-on.git/issues",
|
||||||
|
"category": "Mesh",
|
||||||
|
}
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
def isEven(num):
|
||||||
|
if num % 2 == 0:
|
||||||
|
return True
|
||||||
|
else: return False
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectFromGeometry(bpy.types.Operator):
|
||||||
|
"""Create an object from selected geometry""" # Use this as a tooltip for menu items and buttons.
|
||||||
|
bl_idname = "object.new_from_geometry" # Unique identifier for buttons and menu items to reference.
|
||||||
|
bl_label = "New object from selected geometry" # Display name in the interface.
|
||||||
|
bl_options = {'REGISTER', 'UNDO'} # Enable undo for the operator.
|
||||||
|
|
||||||
|
#it doesn't work... could not find a way to disable properties/options
|
||||||
|
#def modeOptionsChanged(self, context):
|
||||||
|
# if self.modeOptions == "useVerts": # enable verts options
|
||||||
|
# self.drawEdges.options = {}
|
||||||
|
# self.evenOdd.options = {}
|
||||||
|
# self.joinEnds.options = {}
|
||||||
|
# else: # disable verts options
|
||||||
|
# self.drawEdges.options = {'HIDDEN'}
|
||||||
|
# self.evenOdd.options = {'HIDDEN'}
|
||||||
|
# self.joinEnds.options = {'HIDDEN'}
|
||||||
|
|
||||||
|
modeOptions: bpy.props.EnumProperty(
|
||||||
|
items=[
|
||||||
|
("useFaces", "Object from Faces", "Create object from faces", "", 0),
|
||||||
|
("useEdges", "Object from Edges", "Create object from edges", "", 1),
|
||||||
|
("useVerts", "Object from Vertices", "Create object from vertices", "", 2),
|
||||||
|
],
|
||||||
|
name = "Mode",
|
||||||
|
#update = modeOptionsChanged,
|
||||||
|
default = "useVerts",
|
||||||
|
options = {"SKIP_SAVE"} #if the option is saved, the user will get an error if not selecting the same kind of geometry
|
||||||
|
)
|
||||||
|
|
||||||
|
drawEdges: bpy.props.BoolProperty(name="Draw edges", description="Only works for the Object from Vertices mode.\
|
||||||
|
If checked, edges are drawn between the vertices",
|
||||||
|
default=True)
|
||||||
|
evenOdd: bpy.props.BoolProperty(name="Even-Odd", description="Only works for the Object from Vertices mode.\
|
||||||
|
If checked, the edges are created between even vertices and between odd vertices. Need to select at least 4 vertices",
|
||||||
|
default=False)
|
||||||
|
joinEnds: bpy.props.BoolProperty(name='Join ends', description='Only works for the Object from Vertices mode.\
|
||||||
|
If checked, an edge is added between the first and last selected vertices', default=True)
|
||||||
|
|
||||||
|
def execute(self, context): # execute() is called when running the operator.
|
||||||
|
import bmesh
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT') # needed to update the vertex selection
|
||||||
|
activeObj = context.view_layer.objects.active
|
||||||
|
meshCopy = activeObj.data.copy() # keeping the copy of the active object data for the new object
|
||||||
|
selectedVerts = []
|
||||||
|
selectedEdges = []
|
||||||
|
selectedFaces = []
|
||||||
|
bm = bmesh.new()
|
||||||
|
selectedObjs = bpy.context.view_layer.objects.selected
|
||||||
|
originalObjs = [] # cannot use selectedObjs because it is being changed with the selection
|
||||||
|
copyObjs = [] # copies/duplicates are needed to change the origins without affecting the originals
|
||||||
|
for o in selectedObjs:
|
||||||
|
originalObjs.append(o)
|
||||||
|
for o in originalObjs:
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
o.select_set(True)
|
||||||
|
bpy.ops.object.duplicate() # need to duplicate the mesh to stop affecting the originals
|
||||||
|
o.select_set(False)
|
||||||
|
copyObjs.append(bpy.context.view_layer.objects.selected[0])
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for o in copyObjs:
|
||||||
|
o.select_set(True)
|
||||||
|
bpy.ops.object.origin_set(type='ORIGIN_CURSOR') # using to keep the geometry position relative to the cursor
|
||||||
|
for o in copyObjs:
|
||||||
|
bm.from_mesh(o.data)
|
||||||
|
bm.verts.ensure_lookup_table()
|
||||||
|
for vert in bm.verts: # remove non-selected vertices
|
||||||
|
if not vert.select:
|
||||||
|
bm.verts.remove(vert)
|
||||||
|
if len(bm.select_history) == len(bm.verts): # vertices selected individually, using order of selection
|
||||||
|
selectedVerts.extend(bm.select_history)
|
||||||
|
else: selectedVerts.extend(bm.verts)
|
||||||
|
for edge in bm.edges:
|
||||||
|
if not edge.select:
|
||||||
|
bm.edges.remove(edge)
|
||||||
|
selectedEdges.extend(bm.edges)
|
||||||
|
for face in bm.faces:
|
||||||
|
if not face.select:
|
||||||
|
bm.faces.remove(face)
|
||||||
|
selectedFaces.extend(bm.faces)
|
||||||
|
selectedVerts = list(dict.fromkeys(selectedVerts)) # remove duplicates
|
||||||
|
if len(selectedVerts) < 3:
|
||||||
|
self.report({'ERROR'}, "You need to select at least 3 vertices, or 2 edges, or 1 face")
|
||||||
|
for o in copyObjs: # removing copies
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
for o in originalObjs:
|
||||||
|
o.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = activeObj
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bm.free()
|
||||||
|
return {'CANCELLED'}
|
||||||
|
if self.modeOptions == "useVerts":
|
||||||
|
bmVerts = bmesh.new()
|
||||||
|
for v in selectedVerts:
|
||||||
|
bmVerts.verts.new(v.co)
|
||||||
|
for v in bmVerts.verts:
|
||||||
|
v.select_set(True)
|
||||||
|
bmVerts.verts.ensure_lookup_table()
|
||||||
|
if self.drawEdges == True:
|
||||||
|
numVertices = len(bmVerts.verts)
|
||||||
|
if self.evenOdd == False:
|
||||||
|
if self.joinEnds == True:
|
||||||
|
for i in range(numVertices):
|
||||||
|
bmVerts.edges.new((bmVerts.verts[i-1],bmVerts.verts[i]))
|
||||||
|
else:
|
||||||
|
for i in range(numVertices-1):
|
||||||
|
bmVerts.edges.new((bmVerts.verts[i],bmVerts.verts[i+1]))
|
||||||
|
else:
|
||||||
|
if numVertices < 4:
|
||||||
|
self.report({'ERROR'}, "You need to select at least 4 vertices if the EvenOdd option is enabled")
|
||||||
|
for o in copyObjs: # removing copies
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
for o in originalObjs:
|
||||||
|
o.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = activeObj
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bmVerts.free()
|
||||||
|
bm.free()
|
||||||
|
return {'CANCELLED'}
|
||||||
|
for i in range(numVertices-2):
|
||||||
|
bmVerts.edges.new((bmVerts.verts[i],bmVerts.verts[i+2]))
|
||||||
|
if self.joinEnds == True:
|
||||||
|
bmVerts.edges.new((bmVerts.verts[numVertices-1],bmVerts.verts[0]))
|
||||||
|
bmVerts.to_mesh(meshCopy)
|
||||||
|
ob = bpy.data.objects.new('objectFromVertices',meshCopy)
|
||||||
|
ob.location = context.scene.cursor.location
|
||||||
|
context.collection.objects.link(ob)
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
bpy.context.view_layer.objects.active = ob
|
||||||
|
if self.modeOptions == "useEdges":
|
||||||
|
if len(selectedEdges) < 2:
|
||||||
|
self.report({'ERROR'}, "You need to select at least 2 edges for the Object from Edges mode")
|
||||||
|
for o in copyObjs: # removing copies
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
for o in originalObjs:
|
||||||
|
o.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = activeObj
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bm.free()
|
||||||
|
return {'CANCELLED'}
|
||||||
|
bmEdges = bmesh.new()
|
||||||
|
for e in selectedEdges:
|
||||||
|
vertsList = []
|
||||||
|
for v in e.verts:
|
||||||
|
bmEdges.verts.new(v.co)
|
||||||
|
bmEdges.verts.ensure_lookup_table()
|
||||||
|
vertsList.append(bmEdges.verts[len(bmEdges.verts) - 1])
|
||||||
|
bmEdges.edges.new(vertsList)
|
||||||
|
bmesh.ops.remove_doubles(bmEdges,verts=bmEdges.verts,dist=0.001)
|
||||||
|
bmEdges.edges.ensure_lookup_table()
|
||||||
|
for e in bmEdges.edges:
|
||||||
|
e.select_set(True)
|
||||||
|
bmEdges.to_mesh(meshCopy)
|
||||||
|
ob = bpy.data.objects.new('objectFromEdges',meshCopy)
|
||||||
|
ob.location = context.scene.cursor.location
|
||||||
|
context.collection.objects.link(ob)
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
bpy.context.view_layer.objects.active = ob
|
||||||
|
if self.modeOptions == "useFaces":
|
||||||
|
if len(selectedFaces) < 1:
|
||||||
|
self.report({'ERROR'}, "You need to select at least 1 face for the Object from Faces mode")
|
||||||
|
for o in copyObjs: # removing copies
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
for o in originalObjs:
|
||||||
|
o.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = activeObj
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bm.free()
|
||||||
|
return {'CANCELLED'}
|
||||||
|
bmFaces = bmesh.new()
|
||||||
|
for f in selectedFaces:
|
||||||
|
vertsList = []
|
||||||
|
for v in f.verts:
|
||||||
|
bmFaces.verts.new(v.co)
|
||||||
|
bmFaces.verts.ensure_lookup_table()
|
||||||
|
vertsList.append(bmFaces.verts[len(bmFaces.verts) - 1])
|
||||||
|
bmFaces.faces.new(vertsList)
|
||||||
|
bmesh.ops.remove_doubles(bmFaces,verts=bmFaces.verts,dist=0.001)
|
||||||
|
bmFaces.faces.ensure_lookup_table()
|
||||||
|
for f in bmFaces.faces:
|
||||||
|
f.select_set(True)
|
||||||
|
bmFaces.to_mesh(meshCopy)
|
||||||
|
ob = bpy.data.objects.new('objectFromFaces',meshCopy)
|
||||||
|
ob.location = context.scene.cursor.location
|
||||||
|
context.collection.objects.link(ob)
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
bpy.context.view_layer.objects.active = ob
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
for o in copyObjs: # removing copies
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
bm.free()
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def menu_func(self, context):
|
||||||
|
self.layout.operator(ObjectFromGeometry.bl_idname)
|
||||||
|
|
||||||
|
# store keymaps here to access after registration
|
||||||
|
addon_keymaps = []
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_class(ObjectFromGeometry)
|
||||||
|
bpy.types.VIEW3D_MT_edit_mesh.append(menu_func) # Adds the new operator to an existing menu.
|
||||||
|
|
||||||
|
# handle the keymap
|
||||||
|
wm = bpy.context.window_manager
|
||||||
|
# Note that in background mode (no GUI available), keyconfigs are not available either,
|
||||||
|
# so we have to check this to avoid nasty errors in background case.
|
||||||
|
kc = wm.keyconfigs.addon
|
||||||
|
if kc:
|
||||||
|
km = wm.keyconfigs.addon.keymaps.new(name='Mesh', space_type='EMPTY')
|
||||||
|
kmi = km.keymap_items.new(ObjectFromGeometry.bl_idname, 'O', 'PRESS', alt=True)
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
# Note: when unregistering, it's usually good practice to do it in reverse order you registered.
|
||||||
|
# Can avoid strange issues like keymap still referring to operators already unregistered...
|
||||||
|
# handle the keymap
|
||||||
|
for km, kmi in addon_keymaps:
|
||||||
|
km.keymap_items.remove(kmi)
|
||||||
|
addon_keymaps.clear()
|
||||||
|
|
||||||
|
bpy.utils.unregister_class(ObjectFromGeometry)
|
||||||
|
bpy.types.VIEW3D_MT_edit_mesh.remove(menu_func)
|
||||||
|
|
||||||
|
|
||||||
|
# This allows you to run the script directly from Blender's Text editor
|
||||||
|
# to test the add-on without having to install it.
|
||||||
|
if __name__ == "__main__":
|
||||||
|
register()
|
Reference in New Issue
Block a user