def cloth_fill_main2(context, loop_obj, oct, smooth, debug = False): ''' notes: make sure the user view is such that you can see the entire ring with out any corosses (knots) if calling from script not in 3dview, you can override the view args: context - blender context loop_obj: blender curve object or mesh object representing just a loop oct - octree depth for the grid to fill. smooth - iteartions to smooth the surface (soap bubble effect) return: CurveMesh : The filled looop object type Blender Object (Mesh) ''' #selection mode = verts sce = context.scene if context.area.type != 'VIEW_3D': # Py cant access notifers for area in context.window.screen.areas: if area.type == 'VIEW_3D': # for reg in area.regions: # if reg.type == 'WINDOW': # region = reg for spc in area.spaces: if spc.type == 'VIEW_3D': v3d = spc region = spc.region_3d else: #get the space data v3d = bpy.context.space_data #v3d.transform_orientation = 'GLOBAL' #v3d.pivot_point = 'MEDIAN_POINT' region = v3d.region_3d vrot = region.view_rotation #this is a quat Z = vrot * Vector((0,0,1)) #change the mesh orientation to align with view..for blockout odcutils.reorient_object(loop_obj, vrot) #TODO test this sce.update() if loop_obj.parent: reparent = True else: reparent = False if loop_obj.type not in {'CURVE', 'MESH'}: return me = loop_obj.to_mesh(context.scene, True, 'PREVIEW') tray_bme = bmesh.new() tray_bme.from_mesh(me) tray_me = bpy.data.meshes.new('cloth tray') TrayOb = bpy.data.objects.new('Cloth Tray', tray_me) TrayOb.matrix_world = loop_obj.matrix_world #do some size estimation min_size = min(loop_obj.dimensions) size = max(list(loop_obj.dimensions)) grid_predict = size * 0.9 / pow(oct,2) print("grid prediction is: " + str(grid_predict)) #make a 2nd object to project down on... tmp_bme = bmesh.new() tmp_bme.from_mesh(me) temp_me = bpy.data.meshes.new('cloth temp') TempOb = bpy.data.objects.new('Cloth Temp', temp_me) TempOb.matrix_world = loop_obj.matrix_world context.scene.objects.link(TempOb) #fill the the surface of the temp geom = bmesh.ops.triangle_fill(tmp_bme, use_beauty = True, edges = tmp_bme.edges) tmp_bme.edges.ensure_lookup_table() tmp_bme.verts.ensure_lookup_table() #move edges outward a little perim_eds = [ed for ed in tmp_bme.edges if not ed.is_manifold] odcutils.extrude_bmesh_loop(tmp_bme, perim_eds, loop_obj.matrix_world, Z, -.001 * min_size, move_only = True) #This time add an extrusion odcutils.extrude_bmesh_loop(tmp_bme, perim_eds, loop_obj.matrix_world, Z, -.05 * min_size, move_only = False) #put the data in tmp_bme.to_mesh(TempOb.data) tmp_bme.free() #Now deal with the tray which is to be remeshed. #first, solidify all of their #flatten. Since oriented into view, this flattens it for v in tray_bme.verts: v.co[2] = 0.00 #triangle fill geom = bmesh.ops.triangle_fill(tray_bme, use_beauty = True, edges = tray_bme.edges) solid_geom = [f for f in tray_bme.faces] + [v for v in tray_bme.verts] + [ed for ed in tray_bme.edges] new_geom = bmesh.ops.solidify(tray_bme, geom = solid_geom, thickness = grid_predict * 0.75) tray_bme.to_mesh(tray_me) tray_bme.free() context.scene.objects.link(TrayOb) mod = TrayOb.modifiers.new('Remesh', type = 'REMESH') mod.octree_depth = oct mod.scale = .9 tray_bme = bmesh.new() final_me = TrayOb.to_mesh(context.scene, apply_modifiers = True, settings = 'PREVIEW') tray_bme.from_mesh(final_me) tray_bme.verts.ensure_lookup_table() to_delete = [] for v in tray_bme.verts: if v.co[2] > .0001 or v.co[2] < -.0001: to_delete.append(v) bmesh.ops.delete(tray_bme, geom = to_delete, context = 1) TrayOb.modifiers.remove(modifier = mod) tray_bme.to_mesh(TrayOb.data) tray_bme.free() #TODOD, manually with ray cast swrap = TrayOb.modifiers.new('Shrinkwrap', type = 'SHRINKWRAP') swrap.wrap_method = 'PROJECT' swrap.use_project_z = True swrap.use_negative_direction = True swrap.use_positive_direction = True swrap.target = TempOb #tray_me = TrayOb.to_mesh(context.scene, apply_modifiers = True, settings = 'PREVIEW') #TrayOb.data = tray_me #TrayOb.modifiers.remove(modifier = swrap) if reparent: TrayOb.update_tag() sce.update() odcutils.parent_in_place(TrayOb, loop_obj.parent) return TrayOb
def teeth_to_curve(context, arch, sextant, tooth_library, teeth = [], shift = 'BUCCAL', limit = False, link = False, reverse = False, mirror = False, debug = False, reorient = True): ''' puts teeth along a curve for full arch planning args: curve - blender Curve object sextant - the quadrant or sextant that the curve corresponds to. enum in 'MAX', 'MAND', 'UR' 'LR' 'LR' 'LL' 'UA' 'LA' ' teeth - list of odc_teeth, to link to or from. eg, if tooth already a restoration it will use that object, if not, it will link a new blender object to that tooth as the restoration or contour. shift = whether to use buccal cusps, center of mass or, center of fossa to align onto cirve. enum in 'BUCCAL', 'COM', 'FOSSA' limit - only link teeth for each tooth in teeth link - Bool, whether or not to link to/from the teeth list ''' if debug: start = time.time() orig_arch_name = arch.name bpy.ops.object.select_all(action='DESELECT') context.scene.objects.active = arch arch.hide = False arch.select = True if mirror: #This should help with the mirroring? arch.data.resolution_u = 5 #if it doesn't have a mirror, we need to mirror it if "Mirror" not in arch.modifiers: bpy.ops.object.modifier_add(type='MIRROR') #non mirrored curve needed for appropriate constraining.. #convert to mesh applies mirror, reconvert to curve gives us a full length curve arch.modifiers["Mirror"].merge_threshold = 5 bpy.ops.object.convert(target='MESH',keep_original = True) bpy.ops.object.convert(target='CURVE', keep_original = False) #this will be the new full arch arch = context.object arch.name = orig_arch_name + "_Mirrored" #we may want to switch the direction of the curve :-) #we may also want to handle this outside of this function if reverse: bpy.ops.object.mode_set(mode='EDIT') bpy.ops.curve.switch_direction() bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.convert(target='MESH', keep_original = True) arch_mesh = context.object #now the mesh conversion arch_len = 0 mx = arch_mesh.matrix_world #do some calcs to the curve #TODO: split this method off. It may already #be in odcutils. occ_dir = Vector((0,0,0)) #this will end be a normalized, global direction for i in range(0,len(arch_mesh.data.vertices)-1): v0 = arch_mesh.data.vertices[i] v1 = arch_mesh.data.vertices[i+1] V0 = mx*v1.co - mx*v0.co arch_len += V0.length if i < len(arch_mesh.data.vertices)-2: v2 = arch_mesh.data.vertices[i+2] V1 = mx*v2.co - mx*v1.co occ_dir += V0.cross(V1) if debug: print("arch is %f mm long" % arch_len) #pull values from the tooth size/data #if we are mirroring, we need to do some logic if mirror: if sextant not in ["UR","UL","LR","LL","MAX","MAND"]: print('Incorrect sextant for mirroring') return {'CANCELLED'} else: if sextant.startswith("U"): sextant = "MAX" elif sextant.startswith("L"): sextant = "MAND" #else..leave quadrant alone curve_teeth = quadrant_dict[sextant] occ_dir *= occ_direct_dict[sextant] * 1/(len(arch_mesh.data.vertices)-2) occ_dir.normalize() #this deletes the arch mesh...not the arch curve bpy.ops.object.delete() if reorient: arch_z = mx.to_quaternion() * Vector((0,0,1)) arch_z.normalize() if math.pow(arch_z.dot(occ_dir),2) < .9: orient = odcutils.rot_between_vecs(Vector((0,0,1)), occ_dir) #align the local Z of bezier with occlusal direction (which is global). odcutils.reorient_object(arch, orient) if debug: print("working on these teeth %s" % ":".join(curve_teeth)) #import/link teeth from the library restorations = [] if link and len(context.scene.odc_teeth): for tooth in context.scene.odc_teeth: if tooth.name[0:2] in curve_teeth: #TODO: restoration etc? #we will have to check later if we need to use the restoration #from this tooth restorations.append(tooth.name) if debug: print("These restorations are already in the proposed quadrant %s" % ", ".join(restorations)) #figure out which objects we are going to distribute. lib_teeth_names = odcutils.obj_list_from_lib(tooth_library) #TODO: check if tooth_library is valid? tooth_objects=[[None]]*len(curve_teeth) #we want this list to be mapped to curve_teeth with it's index...dictionary if we have to delete_later = [] for i, planned_tooth in enumerate(curve_teeth): #this will be a one item list tooth_in_scene = [tooth for tooth in context.scene.odc_teeth if tooth.name.startswith(planned_tooth)] if link and len(tooth_in_scene): #check if the restoration is already there...if so, use it if tooth_in_scene[0].contour: tooth_objects[i] = bpy.data.objects[tooth_in_scene[0].contour] #if it's not there, add it in, and associate it with ODCTooth Object else: for tooth in lib_teeth_names: if tooth.startswith(planned_tooth): #necessary that the planned teeth have logical names new_name = tooth + "_ArchPlanned" if new_name in bpy.data.objects: ob = bpy.data.objects[new_name] me = ob.data ob.user_clear() bpy.data.objects.remove(ob) bpy.data.meshes.remove(me) context.scene.update() odcutils.obj_from_lib(tooth_library, tooth) ob = bpy.data.objects[tooth] context.scene.objects.link(ob) ob.name = new_name tooth_objects[i] = ob tooth_in_scene[0].contour = ob.name break #in case there are multiple copies? else: #the tooth is not existing restoration, and we want to put it in anyway for tooth in lib_teeth_names: if tooth.startswith(planned_tooth): new_name = tooth + "_ArchPlanned" if new_name in bpy.data.objects: ob = bpy.data.objects[new_name] me = ob.data ob.user_clear() bpy.data.objects.remove(ob) bpy.data.meshes.remove(me) context.scene.update() odcutils.obj_from_lib(tooth_library, tooth) ob = bpy.data.objects[tooth] ob.name += "_ArchPlanned" if limit: context.scene.objects.link(ob) delete_later.append(ob) else: context.scene.objects.link(ob) tooth_objects[i]= ob break if debug: print(tooth_objects) #secretly, we imported the whole quadrant..we will delete them later teeth_len = 0 lengths = [[0]] * len(curve_teeth) #list of tooth mesial/distal lengths locs = [[0]] * len(curve_teeth) #normalized list of locations for i, ob in enumerate(tooth_objects): lengths[i] = ob.dimensions[0] teeth_len += ob.dimensions[0] locs[i] = teeth_len - ob.dimensions[0]/2 scale = arch_len/teeth_len crowding = teeth_len - arch_len if debug > 1: print(lengths) print(locs) print(scale) print("there is %d mm of crowding" % round(crowding,2)) print("there is a %d pct archlength discrepancy" % round(100-scale*100, 2)) #scale them to the right size for i, ob in enumerate(tooth_objects): if shift == 'FOSSA': delta = .05 else: delta = 0 #resize it ob.scale[0] *= scale + delta ob.scale[1] *= scale + delta ob.scale[2] *= scale + delta #find the location of interest we want? # bbox center, cusp tip? fossa/grove, incisal edge? #TODO: odcutils.tooth_features(tooth,feature) (world coords or local?) ob.location = Vector((0,0,0)) if ob.rotation_mode != 'QUATERNION': ob.rotation_mode = 'QUATERNION' ob.rotation_quaternion = Quaternion((1,0,0,0)) #center line...we want palatinal face median point z,y with midpointx and center line min local z #buccal line...we want incisal edge median local y, maxlocal z, midpoing bbox x and buccal cusp max z? context.scene.objects.active = ob ob.select = True ob.hide = False ob.constraints.new('FOLLOW_PATH') path_constraint = ob.constraints["Follow Path"] path_constraint.target = arch path_constraint.use_curve_follow = True #find out if we cross the midline if sextant in ['MAX','MAND','UA','LA']: path_constraint.forward_axis = 'FORWARD_X' if int(curve_teeth[i]) > 20 and int(curve_teeth[i]) < 40: path_constraint.forward_axis = 'TRACK_NEGATIVE_X' else: path_constraint.forward_axis = 'FORWARD_X' path_constraint.offset = 100*(-1 + locs[i]/teeth_len) #after arranging them on the curve, make a 2nd pass to spin them or not #decrease in number means mesial. Except at midline.this will happen #we have constructed curve_teeth such that there will never be a non #integer change in adjacent list members. #eg, #quaternion rotation rules # Qtotal = Qa * Qb represtnts rotation b followed by rotation a #what we are doing is testing the occlusal direction of one tooth vs the arch occlusal direction context.scene.update() ob_dist = tooth_objects[1] ob_mes = tooth_objects[0] mesial = int(curve_teeth[1]) - int(curve_teeth[0]) == 1 #if true....distal numbers > mesial numbers vect = ob_mes.matrix_world * ob_mes.location - ob_dist.matrix_world * ob_dist.location spin = (vect.dot(ob_dist.matrix_world.to_quaternion() * Vector((1,0,0))) < 0) == mesial tooth_occ = ob_mes.matrix_world.to_quaternion() * Vector((0,0,1)) flip = tooth_occ.dot(occ_dir) > 0 if debug: print('We will flip the teeth: %s. We will spin the teeth: %s.' % (str(flip), str(spin))) for ob in tooth_objects: if flip: ob.rotation_quaternion = Quaternion((0,1,0,0)) if spin: ob.rotation_quaternion = Quaternion((0,0,0,1)) * ob.rotation_quaternion for i, ob in enumerate(tooth_objects): if shift == 'BUCCAL': groups = ["Incisal Edge", "Distobuccal Cusp","Mesiobuccal Cusp", "Buccal Cusp"] inds = [] for vgroup in groups: if vgroup in ob.vertex_groups: inds += odcutils.vert_group_inds_get(context, ob, vgroup) max_z = 0 max_ind = 0 for j in inds: z = ob.data.vertices[j].co[2] if z > max_z: max_ind = j max_z = z tip = ob.data.vertices[max_ind].co tooth_shift = Vector((0,tip[1]*ob.scale[1],tip[2]*ob.scale[2])) if sextant in ['MAX','MAND','UA','LA']: #no freakin idea why this is happening, but empirically, it's working tooth_shift[1]*= -1 ob.location += (-1 + 2*flip) * tooth_shift if shift == 'FOSSA': groups = ["Middle Fissure", "Palatinal Face"] inds = [] for vgroup in groups: if vgroup in ob.vertex_groups and vgroup == "Middle Fissure": inds += odcutils.vert_group_inds_get(context, ob, vgroup) min_z = ob.dimensions[2] min_ind = 0 for j in inds: z = ob.data.vertices[j].co[2] if z < min_z: min_ind = j min_z = z depth = ob.data.vertices[min_ind].co tooth_shift = Vector((0,depth[1]*ob.scale[1],depth[2]*ob.scale[2])) elif vgroup in ob.vertex_groups and vgroup == "Palatinal Face": inds += odcutils.vert_group_inds_get(context, ob, vgroup) mx = Matrix.Identity(4) com = odcutils.get_com(ob.data, inds, mx) tooth_shift = odcutils.scale_vec_mult(com, ob.matrix_world.to_scale()) if sextant in ['MAX','MAND','UA','LA']: #no freakin idea why this is happening, but empirically, it's working tooth_shift[1]*= -1 ob.location += (-1 + 2*flip) * tooth_shift if limit: bpy.ops.object.select_all(action='DESELECT') for ob in delete_later: ob.select = True context.scene.objects.active = ob bpy.ops.object.delete()
def cloth_fill_main(context, loop_obj, oct, smooth, debug = False): ''' notes: make sure the user view is such that you can see the entire ring with out any corosses (knots) if calling from script not in 3dview, you can override the view args: context - blender context loop_obj: blender curve object or mesh object representing just a loop oct - octree depth for the grid to fill. smooth - iteartions to smooth the surface (soap bubble effect) return: CurveMesh : The filled looop object type Blender Object (Mesh) ''' #selection mode = verts sce = context.scene context.tool_settings.mesh_select_mode = [True,False,False] #get the space data v3d = bpy.context.space_data v3d.transform_orientation = 'GLOBAL' v3d.pivot_point = 'MEDIAN_POINT' region = v3d.region_3d vrot = region.view_rotation #this is a quat #set object mode...force selection if context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') context.scene.objects.active = loop_obj loop_obj.select = True #change the mesh orientation to align with view..for blockout odcutils.reorient_object(loop_obj, vrot) #TODO test this sce.update() if loop_obj.type in {'CURVE','MESH'}: if loop_obj.type == 'CURVE': #check if cyclic if not loop_obj.data.splines[0].use_cyclic_u: loop_obj.data.splines[0].use_cyclic_u = True #TODO: add this over to margin #convert the curve to a mesh...so we can use it. bpy.ops.object.duplicate() bpy.ops.object.convert(target='MESH', keep_original = False) #active object is now the mesh version of the curve else: #make sure it's a loop if len(loop_obj.data.vertices) != len(loop_obj.data.edges): print('this is not a loop') return else: bpy.ops.object.duplicate() #active object is now the mesh duplicate #this will become our final cloth filled objec CurveMesh = context.object CurveMesh.name = "Cloth Tray Mesh" #do some size estimation size = max(list(CurveMesh.dimensions)) grid_predict = size * 0.9 / pow(oct,2) print("grid prediction is: " + str(grid_predict)) #make a duplicate... current_objects = list(bpy.data.objects) #remember current objects to ID new ones later bpy.ops.object.duplicate() for obj in sce.objects: if obj not in current_objects: obj.name = "cloth_temp" Temp = obj #fill the the surface of the temp bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.fill() #stretch it out a little bpy.ops.mesh.select_all(action = 'DESELECT') bpy.context.tool_settings.mesh_select_mode = [False,True,False] bpy.ops.mesh.select_non_manifold() bpy.ops.object.editmode_toggle() eds = [ed for ed in Temp.data.edges if ed.select] barrier = .05 * min(Temp.dimensions) odcutils.extrude_edges_out_view(Temp.data, eds, Temp.matrix_world, barrier/5, debug = debug) bpy.ops.object.editmode_toggle() bpy.context.tool_settings.mesh_select_mode = [False,True,False] bpy.ops.mesh.select_non_manifold() bpy.ops.mesh.extrude_edges_move() bpy.ops.object.editmode_toggle() eds = [ed for ed in Temp.data.edges if ed.select] odcutils.extrude_edges_out_view(Temp.data, eds, Temp.matrix_world, barrier, debug = debug) bpy.ops.object.editmode_toggle() bpy.context.tool_settings.mesh_select_mode = [True,False,False] bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') CurveMesh.select = True sce.objects.active = CurveMesh CurveMesh.rotation_mode = 'QUATERNION' #make the origin the same as the bez curve? sce.cursor_location = loop_obj.location bpy.ops.object.origin_set(type = 'ORIGIN_CURSOR') if CurveMesh.parent: Parent = CurveMesh.parent reparent = True wmx = CurveMesh.matrix_world.copy() CurveMesh.parent = None CurveMesh.matrix_world = wmx else: wmx = Matrix.Identity(4) reparent = False #unrotate it so we can make a nice remesh surface #although why this doesn't work with local coords I dunno bpy.ops.object.rotation_clear() #flatten to view...fill in the loop bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') #get the space data v3d = bpy.context.space_data v3d.transform_orientation = 'LOCAL' v3d.pivot_point = 'MEDIAN_POINT' bpy.ops.transform.resize(value=(1, 1, 0), constraint_orientation='LOCAL') bpy.ops.mesh.looptools_space() bpy.ops.mesh.fill() #add modifiers bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.modifier_add(type='SOLIDIFY') bpy.ops.object.modifier_add(type='REMESH') solmod = CurveMesh.modifiers["Solidify"] solmod.thickness = grid_predict * .75 remod = CurveMesh.modifiers["Remesh"] remod.octree_depth = oct remod.scale = .9 #for some reason the modifier weren't updating bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() #problem with applying modifiers...new method. mesh = CurveMesh.to_mesh(bpy.context.scene, True, 'RENDER') new_obj = bpy.data.objects.new(CurveMesh.name, mesh) bpy.context.scene.objects.link(new_obj) new_obj.matrix_world = wmx bpy.context.scene.objects.active = new_obj CurveMesh.select = True bpy.context.scene.objects.active = CurveMesh bpy.ops.object.delete() new_obj.select = True CurveMesh = new_obj bpy.context.scene.objects.active = CurveMesh ''' bpy.ops.object.modifier_apply(modifier="Solidify") bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() bpy.ops.object.modifier_apply(modifier="Remesh") ''' bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action = 'DESELECT') bpy.ops.object.mode_set(mode = 'OBJECT') #make it a 2d obejct bpy.context.tool_settings.mesh_select_mode = [False,False,True] flat = False n = 0 while not flat: #hope to select a polygon not on the border CurveMesh.data.polygons[n].select = True bpy.ops.object.mode_set(mode = 'EDIT') bpy.ops.mesh.faces_select_linked_flat() bpy.ops.object.mode_set(mode = 'OBJECT') sel_faces = [poly for poly in CurveMesh.data.polygons if poly.select] if len(sel_faces) > len(CurveMesh.data.polygons)/3: flat = True if n > 100: break n+= 1 bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.delete() bpy.context.tool_settings.mesh_select_mode = [False,True,False] bpy.ops.mesh.select_loose() bpy.ops.mesh.delete(type='EDGE') bpy.ops.mesh.select_non_manifold() bpy.context.tool_settings.mesh_select_mode = [True,False,False] bpy.ops.mesh.select_all(action='INVERT') bpy.ops.mesh.vertices_smooth(repeat = smooth) bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.modifier_add(type='SHRINKWRAP') swrap = CurveMesh.modifiers["Shrinkwrap"] swrap.wrap_method = 'PROJECT' swrap.use_project_z = True swrap.use_negative_direction = True swrap.use_positive_direction = True swrap.target = Temp CurveMesh.rotation_quaternion = vrot bpy.ops.object.modifier_apply(modifier="Shrinkwrap") if reparent: CurveMesh.update_tag() sce.update() odcutils.parent_in_place(CurveMesh, Parent) bpy.ops.object.select_all(action='DESELECT') if debug < 3: Temp.select = True sce.objects.active=Temp bpy.ops.object.delete() CurveMesh.select = True sce.objects.active = CurveMesh bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='DESELECT') bpy.context.tool_settings.mesh_select_mode = [False,True,False] bpy.ops.mesh.select_loose() bpy.ops.mesh.delete(type='EDGE') bpy.context.tool_settings.mesh_select_mode = [True,False,False] bpy.ops.object.mode_set(mode='OBJECT') mx = CurveMesh.matrix_world sum_edges = 0 if len(CurveMesh.data.edges) != 0: for ed in CurveMesh.data.edges: v0 = CurveMesh.data.vertices[ed.vertices[0]] v1 = CurveMesh.data.vertices[ed.vertices[1]] V = mx*v1.co - mx*v0.co sum_edges += V.length avg_edge = sum_edges/len(CurveMesh.data.edges) if debug: print("average grid size: " + str(avg_edge)) return CurveMesh
def teeth_to_curve(context, arch, sextant, tooth_library, teeth=[], shift='BUCCAL', limit=False, link=False, reverse=False, mirror=False, debug=False, reorient=True): ''' puts teeth along a curve for full arch planning args: curve - blender Curve object sextant - the quadrant or sextant that the curve corresponds to. enum in 'MAX', 'MAND', 'UR' 'LR' 'LR' 'LL' 'UA' 'LA' ' teeth - list of odc_teeth, to link to or from. eg, if tooth already a restoration it will use that object, if not, it will link a new blender object to that tooth as the restoration or contour. shift = whether to use buccal cusps, center of mass or, center of fossa to align onto cirve. enum in 'BUCCAL', 'COM', 'FOSSA' limit - only link teeth for each tooth in teeth link - Bool, whether or not to link to/from the teeth list ''' if debug: start = time.time() orig_arch_name = arch.name bpy.ops.object.select_all(action='DESELECT') context.scene.objects.active = arch arch.hide = False arch.select = True if mirror: #This should help with the mirroring? arch.data.resolution_u = 5 #if it doesn't have a mirror, we need to mirror it if "Mirror" not in arch.modifiers: bpy.ops.object.modifier_add(type='MIRROR') #non mirrored curve needed for appropriate constraining.. #convert to mesh applies mirror, reconvert to curve gives us a full length curve arch.modifiers["Mirror"].merge_threshold = 5 bpy.ops.object.convert(target='MESH', keep_original=True) bpy.ops.object.convert( target='CURVE', keep_original=False) #this will be the new full arch arch = context.object arch.name = orig_arch_name + "_Mirrored" #we may want to switch the direction of the curve :-) #we may also want to handle this outside of this function if reverse: bpy.ops.object.mode_set(mode='EDIT') bpy.ops.curve.switch_direction() bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.convert(target='MESH', keep_original=True) arch_mesh = context.object #now the mesh conversion arch_len = 0 mx = arch_mesh.matrix_world #do some calcs to the curve #TODO: split this method off. It may already #be in odcutils. occ_dir = Vector( (0, 0, 0)) #this will end be a normalized, global direction for i in range(0, len(arch_mesh.data.vertices) - 1): v0 = arch_mesh.data.vertices[i] v1 = arch_mesh.data.vertices[i + 1] V0 = mx * v1.co - mx * v0.co arch_len += V0.length if i < len(arch_mesh.data.vertices) - 2: v2 = arch_mesh.data.vertices[i + 2] V1 = mx * v2.co - mx * v1.co occ_dir += V0.cross(V1) if debug: print("arch is %f mm long" % arch_len) #pull values from the tooth size/data #if we are mirroring, we need to do some logic if mirror: if sextant not in ["UR", "UL", "LR", "LL", "MAX", "MAND"]: print('Incorrect sextant for mirroring') return {'CANCELLED'} else: if sextant.startswith("U"): sextant = "MAX" elif sextant.startswith("L"): sextant = "MAND" #else..leave quadrant alone curve_teeth = quadrant_dict[sextant] occ_dir *= occ_direct_dict[sextant] * 1 / (len(arch_mesh.data.vertices) - 2) occ_dir.normalize() #this deletes the arch mesh...not the arch curve bpy.ops.object.delete() if reorient: arch_z = mx.to_quaternion() * Vector((0, 0, 1)) arch_z.normalize() if math.pow(arch_z.dot(occ_dir), 2) < .9: orient = odcutils.rot_between_vecs( Vector((0, 0, 1)), occ_dir ) #align the local Z of bezier with occlusal direction (which is global). odcutils.reorient_object(arch, orient) if debug: print("working on these teeth %s" % ":".join(curve_teeth)) #import/link teeth from the library restorations = [] if link and len(context.scene.odc_teeth): for tooth in context.scene.odc_teeth: if tooth.name[0:2] in curve_teeth: #TODO: restoration etc? #we will have to check later if we need to use the restoration #from this tooth restorations.append(tooth.name) if debug: print( "These restorations are already in the proposed quadrant %s" % ", ".join(restorations)) #figure out which objects we are going to distribute. lib_teeth_names = odcutils.obj_list_from_lib( tooth_library) #TODO: check if tooth_library is valid? tooth_objects = [[None]] * len( curve_teeth ) #we want this list to be mapped to curve_teeth with it's index...dictionary if we have to delete_later = [] for i, planned_tooth in enumerate(curve_teeth): #this will be a one item list tooth_in_scene = [ tooth for tooth in context.scene.odc_teeth if tooth.name.startswith(planned_tooth) ] if link and len(tooth_in_scene): #check if the restoration is already there...if so, use it if tooth_in_scene[0].contour: tooth_objects[i] = bpy.data.objects[tooth_in_scene[0].contour] #if it's not there, add it in, and associate it with ODCTooth Object else: for tooth in lib_teeth_names: if tooth.startswith( planned_tooth ): #necessary that the planned teeth have logical names new_name = tooth + "_ArchPlanned" if new_name in bpy.data.objects: ob = bpy.data.objects[new_name] me = ob.data ob.user_clear() bpy.data.objects.remove(ob) bpy.data.meshes.remove(me) context.scene.update() odcutils.obj_from_lib(tooth_library, tooth) ob = bpy.data.objects[tooth] context.scene.objects.link(ob) ob.name = new_name tooth_objects[i] = ob tooth_in_scene[0].contour = ob.name break #in case there are multiple copies? else: #the tooth is not existing restoration, and we want to put it in anyway for tooth in lib_teeth_names: if tooth.startswith(planned_tooth): new_name = tooth + "_ArchPlanned" if new_name in bpy.data.objects: ob = bpy.data.objects[new_name] me = ob.data ob.user_clear() bpy.data.objects.remove(ob) bpy.data.meshes.remove(me) context.scene.update() odcutils.obj_from_lib(tooth_library, tooth) ob = bpy.data.objects[tooth] ob.name += "_ArchPlanned" if limit: context.scene.objects.link(ob) delete_later.append(ob) else: context.scene.objects.link(ob) tooth_objects[i] = ob break if debug: print(tooth_objects) #secretly, we imported the whole quadrant..we will delete them later teeth_len = 0 lengths = [[0]] * len(curve_teeth) #list of tooth mesial/distal lengths locs = [[0]] * len(curve_teeth) #normalized list of locations for i, ob in enumerate(tooth_objects): lengths[i] = ob.dimensions[0] teeth_len += ob.dimensions[0] locs[i] = teeth_len - ob.dimensions[0] / 2 scale = arch_len / teeth_len crowding = teeth_len - arch_len if debug > 1: print(lengths) print(locs) print(scale) print("there is %d mm of crowding" % round(crowding, 2)) print("there is a %d pct archlength discrepancy" % round(100 - scale * 100, 2)) #scale them to the right size for i, ob in enumerate(tooth_objects): if shift == 'FOSSA': delta = .05 else: delta = 0 #resize it ob.scale[0] *= scale + delta ob.scale[1] *= scale + delta ob.scale[2] *= scale + delta #find the location of interest we want? # bbox center, cusp tip? fossa/grove, incisal edge? #TODO: odcutils.tooth_features(tooth,feature) (world coords or local?) ob.location = Vector((0, 0, 0)) if ob.rotation_mode != 'QUATERNION': ob.rotation_mode = 'QUATERNION' ob.rotation_quaternion = Quaternion((1, 0, 0, 0)) #center line...we want palatinal face median point z,y with midpointx and center line min local z #buccal line...we want incisal edge median local y, maxlocal z, midpoing bbox x and buccal cusp max z? context.scene.objects.active = ob ob.select = True ob.hide = False ob.constraints.new('FOLLOW_PATH') path_constraint = ob.constraints["Follow Path"] path_constraint.target = arch path_constraint.use_curve_follow = True #find out if we cross the midline if sextant in ['MAX', 'MAND', 'UA', 'LA']: path_constraint.forward_axis = 'FORWARD_X' if int(curve_teeth[i]) > 20 and int(curve_teeth[i]) < 40: path_constraint.forward_axis = 'TRACK_NEGATIVE_X' else: path_constraint.forward_axis = 'FORWARD_X' path_constraint.offset = 100 * (-1 + locs[i] / teeth_len) #after arranging them on the curve, make a 2nd pass to spin them or not #decrease in number means mesial. Except at midline.this will happen #we have constructed curve_teeth such that there will never be a non #integer change in adjacent list members. #eg, #quaternion rotation rules # Qtotal = Qa * Qb represtnts rotation b followed by rotation a #what we are doing is testing the occlusal direction of one tooth vs the arch occlusal direction context.scene.update() ob_dist = tooth_objects[1] ob_mes = tooth_objects[0] mesial = int(curve_teeth[1]) - int( curve_teeth[0]) == 1 #if true....distal numbers > mesial numbers vect = ob_mes.matrix_world * ob_mes.location - ob_dist.matrix_world * ob_dist.location spin = (vect.dot(ob_dist.matrix_world.to_quaternion() * Vector( (1, 0, 0))) < 0) == mesial tooth_occ = ob_mes.matrix_world.to_quaternion() * Vector((0, 0, 1)) flip = tooth_occ.dot(occ_dir) > 0 if debug: print('We will flip the teeth: %s. We will spin the teeth: %s.' % (str(flip), str(spin))) for ob in tooth_objects: if flip: ob.rotation_quaternion = Quaternion((0, 1, 0, 0)) if spin: ob.rotation_quaternion = Quaternion( (0, 0, 0, 1)) * ob.rotation_quaternion for i, ob in enumerate(tooth_objects): if shift == 'BUCCAL': groups = [ "Incisal Edge", "Distobuccal Cusp", "Mesiobuccal Cusp", "Buccal Cusp" ] inds = [] for vgroup in groups: if vgroup in ob.vertex_groups: inds += odcutils.vert_group_inds_get(context, ob, vgroup) max_z = 0 max_ind = 0 for j in inds: z = ob.data.vertices[j].co[2] if z > max_z: max_ind = j max_z = z tip = ob.data.vertices[max_ind].co tooth_shift = Vector( (0, tip[1] * ob.scale[1], tip[2] * ob.scale[2])) if sextant in [ 'MAX', 'MAND', 'UA', 'LA' ]: #no freakin idea why this is happening, but empirically, it's working tooth_shift[1] *= -1 ob.location += (-1 + 2 * flip) * tooth_shift if shift == 'FOSSA': groups = ["Middle Fissure", "Palatinal Face"] inds = [] for vgroup in groups: if vgroup in ob.vertex_groups and vgroup == "Middle Fissure": inds += odcutils.vert_group_inds_get(context, ob, vgroup) min_z = ob.dimensions[2] min_ind = 0 for j in inds: z = ob.data.vertices[j].co[2] if z < min_z: min_ind = j min_z = z depth = ob.data.vertices[min_ind].co tooth_shift = Vector((0, depth[1] * ob.scale[1], depth[2] * ob.scale[2])) elif vgroup in ob.vertex_groups and vgroup == "Palatinal Face": inds += odcutils.vert_group_inds_get(context, ob, vgroup) mx = Matrix.Identity(4) com = odcutils.get_com(ob.data, inds, mx) tooth_shift = odcutils.scale_vec_mult( com, ob.matrix_world.to_scale()) if sextant in [ 'MAX', 'MAND', 'UA', 'LA' ]: #no freakin idea why this is happening, but empirically, it's working tooth_shift[1] *= -1 ob.location += (-1 + 2 * flip) * tooth_shift if limit: bpy.ops.object.select_all(action='DESELECT') for ob in delete_later: ob.select = True context.scene.objects.active = ob bpy.ops.object.delete()
def cloth_fill_main2(context, loop_obj, oct, smooth, debug=False): ''' notes: make sure the user view is such that you can see the entire ring with out any corosses (knots) if calling from script not in 3dview, you can override the view args: context - blender context loop_obj: blender curve object or mesh object representing just a loop oct - octree depth for the grid to fill. smooth - iteartions to smooth the surface (soap bubble effect) return: CurveMesh : The filled looop object type Blender Object (Mesh) ''' #selection mode = verts sce = context.scene if context.area.type != 'VIEW_3D': # Py cant access notifers for area in context.window.screen.areas: if area.type == 'VIEW_3D': # for reg in area.regions: # if reg.type == 'WINDOW': # region = reg for spc in area.spaces: if spc.type == 'VIEW_3D': v3d = spc region = spc.region_3d else: #get the space data v3d = bpy.context.space_data #v3d.transform_orientation = 'GLOBAL' #v3d.pivot_point = 'MEDIAN_POINT' region = v3d.region_3d vrot = region.view_rotation #this is a quat Z = vrot * Vector((0, 0, 1)) #change the mesh orientation to align with view..for blockout odcutils.reorient_object(loop_obj, vrot) #TODO test this sce.update() if loop_obj.parent: reparent = True else: reparent = False if loop_obj.type not in {'CURVE', 'MESH'}: return me = loop_obj.to_mesh(context.scene, True, 'PREVIEW') tray_bme = bmesh.new() tray_bme.from_mesh(me) tray_me = bpy.data.meshes.new('cloth tray') TrayOb = bpy.data.objects.new('Cloth Tray', tray_me) TrayOb.matrix_world = loop_obj.matrix_world #do some size estimation min_size = min(loop_obj.dimensions) size = max(list(loop_obj.dimensions)) grid_predict = size * 0.9 / pow(oct, 2) print("grid prediction is: " + str(grid_predict)) #make a 2nd object to project down on... tmp_bme = bmesh.new() tmp_bme.from_mesh(me) temp_me = bpy.data.meshes.new('cloth temp') TempOb = bpy.data.objects.new('Cloth Temp', temp_me) TempOb.matrix_world = loop_obj.matrix_world context.scene.objects.link(TempOb) #fill the the surface of the temp geom = bmesh.ops.triangle_fill(tmp_bme, use_beauty=True, edges=tmp_bme.edges) tmp_bme.edges.ensure_lookup_table() tmp_bme.verts.ensure_lookup_table() #move edges outward a little perim_eds = [ed for ed in tmp_bme.edges if not ed.is_manifold] odcutils.extrude_bmesh_loop(tmp_bme, perim_eds, loop_obj.matrix_world, Z, -.001 * min_size, move_only=True) #This time add an extrusion odcutils.extrude_bmesh_loop(tmp_bme, perim_eds, loop_obj.matrix_world, Z, -.05 * min_size, move_only=False) #put the data in tmp_bme.to_mesh(TempOb.data) tmp_bme.free() #Now deal with the tray which is to be remeshed. #first, solidify all of their #flatten. Since oriented into view, this flattens it for v in tray_bme.verts: v.co[2] = 0.00 #triangle fill geom = bmesh.ops.triangle_fill(tray_bme, use_beauty=True, edges=tray_bme.edges) solid_geom = [f for f in tray_bme.faces] + [v for v in tray_bme.verts] + [ ed for ed in tray_bme.edges ] new_geom = bmesh.ops.solidify(tray_bme, geom=solid_geom, thickness=grid_predict * 0.75) tray_bme.to_mesh(tray_me) tray_bme.free() context.scene.objects.link(TrayOb) mod = TrayOb.modifiers.new('Remesh', type='REMESH') mod.octree_depth = oct mod.scale = .9 tray_bme = bmesh.new() final_me = TrayOb.to_mesh(context.scene, apply_modifiers=True, settings='PREVIEW') tray_bme.from_mesh(final_me) tray_bme.verts.ensure_lookup_table() to_delete = [] for v in tray_bme.verts: if v.co[2] > .0001 or v.co[2] < -.0001: to_delete.append(v) bmesh.ops.delete(tray_bme, geom=to_delete, context=1) TrayOb.modifiers.remove(modifier=mod) tray_bme.to_mesh(TrayOb.data) tray_bme.free() #TODOD, manually with ray cast swrap = TrayOb.modifiers.new('Shrinkwrap', type='SHRINKWRAP') swrap.wrap_method = 'PROJECT' swrap.use_project_z = True swrap.use_negative_direction = True swrap.use_positive_direction = True swrap.target = TempOb #tray_me = TrayOb.to_mesh(context.scene, apply_modifiers = True, settings = 'PREVIEW') #TrayOb.data = tray_me #TrayOb.modifiers.remove(modifier = swrap) if reparent: TrayOb.update_tag() sce.update() odcutils.parent_in_place(TrayOb, loop_obj.parent) return TrayOb
def cloth_fill_main(context, loop_obj, oct, smooth, debug=False): ''' notes: make sure the user view is such that you can see the entire ring with out any corosses (knots) if calling from script not in 3dview, you can override the view args: context - blender context loop_obj: blender curve object or mesh object representing just a loop oct - octree depth for the grid to fill. smooth - iteartions to smooth the surface (soap bubble effect) return: CurveMesh : The filled looop object type Blender Object (Mesh) ''' #selection mode = verts sce = context.scene context.tool_settings.mesh_select_mode = [True, False, False] #get the space data v3d = bpy.context.space_data v3d.transform_orientation = 'GLOBAL' v3d.pivot_point = 'MEDIAN_POINT' region = v3d.region_3d vrot = region.view_rotation #this is a quat #set object mode...force selection if context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') context.scene.objects.active = loop_obj loop_obj.select = True #change the mesh orientation to align with view..for blockout odcutils.reorient_object(loop_obj, vrot) #TODO test this sce.update() if loop_obj.type in {'CURVE', 'MESH'}: if loop_obj.type == 'CURVE': #check if cyclic if not loop_obj.data.splines[0].use_cyclic_u: loop_obj.data.splines[ 0].use_cyclic_u = True #TODO: add this over to margin #convert the curve to a mesh...so we can use it. bpy.ops.object.duplicate() bpy.ops.object.convert(target='MESH', keep_original=False) #active object is now the mesh version of the curve else: #make sure it's a loop if len(loop_obj.data.vertices) != len(loop_obj.data.edges): print('this is not a loop') return else: bpy.ops.object.duplicate() #active object is now the mesh duplicate #this will become our final cloth filled objec CurveMesh = context.object CurveMesh.name = "Cloth Tray Mesh" #do some size estimation size = max(list(CurveMesh.dimensions)) grid_predict = size * 0.9 / pow(oct, 2) print("grid prediction is: " + str(grid_predict)) #make a duplicate... current_objects = list( bpy.data.objects) #remember current objects to ID new ones later bpy.ops.object.duplicate() for obj in sce.objects: if obj not in current_objects: obj.name = "cloth_temp" Temp = obj #fill the the surface of the temp bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.fill() #stretch it out a little bpy.ops.mesh.select_all(action='DESELECT') bpy.context.tool_settings.mesh_select_mode = [False, True, False] bpy.ops.mesh.select_non_manifold() bpy.ops.object.editmode_toggle() eds = [ed for ed in Temp.data.edges if ed.select] barrier = .05 * min(Temp.dimensions) odcutils.extrude_edges_out_view(Temp.data, eds, Temp.matrix_world, barrier / 5, debug=debug) bpy.ops.object.editmode_toggle() bpy.context.tool_settings.mesh_select_mode = [False, True, False] bpy.ops.mesh.select_non_manifold() bpy.ops.mesh.extrude_edges_move() bpy.ops.object.editmode_toggle() eds = [ed for ed in Temp.data.edges if ed.select] odcutils.extrude_edges_out_view(Temp.data, eds, Temp.matrix_world, barrier, debug=debug) bpy.ops.object.editmode_toggle() bpy.context.tool_settings.mesh_select_mode = [True, False, False] bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') CurveMesh.select = True sce.objects.active = CurveMesh CurveMesh.rotation_mode = 'QUATERNION' #make the origin the same as the bez curve? sce.cursor_location = loop_obj.location bpy.ops.object.origin_set(type='ORIGIN_CURSOR') if CurveMesh.parent: Parent = CurveMesh.parent reparent = True wmx = CurveMesh.matrix_world.copy() CurveMesh.parent = None CurveMesh.matrix_world = wmx else: wmx = Matrix.Identity(4) reparent = False #unrotate it so we can make a nice remesh surface #although why this doesn't work with local coords I dunno bpy.ops.object.rotation_clear() #flatten to view...fill in the loop bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') #get the space data v3d = bpy.context.space_data v3d.transform_orientation = 'LOCAL' v3d.pivot_point = 'MEDIAN_POINT' bpy.ops.transform.resize(value=(1, 1, 0), constraint_orientation='LOCAL') bpy.ops.mesh.looptools_space() bpy.ops.mesh.fill() #add modifiers bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.modifier_add(type='SOLIDIFY') bpy.ops.object.modifier_add(type='REMESH') solmod = CurveMesh.modifiers["Solidify"] solmod.thickness = grid_predict * .75 remod = CurveMesh.modifiers["Remesh"] remod.octree_depth = oct remod.scale = .9 #for some reason the modifier weren't updating bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() #problem with applying modifiers...new method. mesh = CurveMesh.to_mesh(bpy.context.scene, True, 'RENDER') new_obj = bpy.data.objects.new(CurveMesh.name, mesh) bpy.context.scene.objects.link(new_obj) new_obj.matrix_world = wmx bpy.context.scene.objects.active = new_obj CurveMesh.select = True bpy.context.scene.objects.active = CurveMesh bpy.ops.object.delete() new_obj.select = True CurveMesh = new_obj bpy.context.scene.objects.active = CurveMesh ''' bpy.ops.object.modifier_apply(modifier="Solidify") bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() bpy.ops.object.modifier_apply(modifier="Remesh") ''' bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') #make it a 2d obejct bpy.context.tool_settings.mesh_select_mode = [False, False, True] flat = False n = 0 while not flat: #hope to select a polygon not on the border CurveMesh.data.polygons[n].select = True bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.faces_select_linked_flat() bpy.ops.object.mode_set(mode='OBJECT') sel_faces = [poly for poly in CurveMesh.data.polygons if poly.select] if len(sel_faces) > len(CurveMesh.data.polygons) / 3: flat = True if n > 100: break n += 1 bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.delete() bpy.context.tool_settings.mesh_select_mode = [False, True, False] bpy.ops.mesh.select_loose() bpy.ops.mesh.delete(type='EDGE') bpy.ops.mesh.select_non_manifold() bpy.context.tool_settings.mesh_select_mode = [True, False, False] bpy.ops.mesh.select_all(action='INVERT') bpy.ops.mesh.vertices_smooth(repeat=smooth) bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.modifier_add(type='SHRINKWRAP') swrap = CurveMesh.modifiers["Shrinkwrap"] swrap.wrap_method = 'PROJECT' swrap.use_project_z = True swrap.use_negative_direction = True swrap.use_positive_direction = True swrap.target = Temp CurveMesh.rotation_quaternion = vrot bpy.ops.object.modifier_apply(modifier="Shrinkwrap") if reparent: CurveMesh.update_tag() sce.update() odcutils.parent_in_place(CurveMesh, Parent) bpy.ops.object.select_all(action='DESELECT') if debug < 3: Temp.select = True sce.objects.active = Temp bpy.ops.object.delete() CurveMesh.select = True sce.objects.active = CurveMesh bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='DESELECT') bpy.context.tool_settings.mesh_select_mode = [False, True, False] bpy.ops.mesh.select_loose() bpy.ops.mesh.delete(type='EDGE') bpy.context.tool_settings.mesh_select_mode = [True, False, False] bpy.ops.object.mode_set(mode='OBJECT') mx = CurveMesh.matrix_world sum_edges = 0 if len(CurveMesh.data.edges) != 0: for ed in CurveMesh.data.edges: v0 = CurveMesh.data.vertices[ed.vertices[0]] v1 = CurveMesh.data.vertices[ed.vertices[1]] V = mx * v1.co - mx * v0.co sum_edges += V.length avg_edge = sum_edges / len(CurveMesh.data.edges) if debug: print("average grid size: " + str(avg_edge)) return CurveMesh