def execute(self, context): tooth = odcutils.tooth_selection( context)[0] #This could theoretically happen to multiple teeth...but not likely sce=bpy.context.scene a = tooth.name prep = tooth.prep_model margin = str(a + "_Margin") Prep = bpy.data.objects[prep] Prep.hide = False master=sce.odc_props.master Master = bpy.data.objects[master] gp_margin = bpy.data.grease_pencil.get(margin + "_tracer") if not gp_margin: self.report("ERROR", "No grease pencil margin trace mark, please 'Initiate Auto Margin' first") return {'CANCELLED'} #Set up the rotation center as the 3d Cursor for A in bpy.context.window.screen.areas: if A.type == 'VIEW_3D': for s in A.spaces: if s.type == 'VIEW_3D': s.pivot_point = 'CURSOR' #set the trasnform orientation to the insertion axis so our z is well definined #this function returns the current transform so we can put it back later current_transform = odcutils.transform_management(tooth, sce, bpy.context.space_data) #Get the prep BBox center for later prep_cent = Vector((0,0,0)) for v in Prep.bound_box: prep_cent = prep_cent + Prep.matrix_world * Vector(v) Prep_Center = prep_cent/8 bpy.ops.gpencil.convert(type = 'PATH') bpy.ops.object.select_all(action='DESELECT') trace_curve = bpy.data.objects[margin + "_tracer"] #refactor to test for new object in scene context.scene.objects.active = trace_curve trace_curve.select = True bpy.ops.object.convert(target = 'MESH') #get our data trace_obj = bpy.context.object trace_data = trace_obj.data #place the intitial shrinkwrap modifier bpy.ops.object.modifier_add(type='SHRINKWRAP') mod = trace_obj.modifiers[0] mod.target=Prep mod.show_in_editmode = True mod.show_on_cage = True bpy.ops.object.modifier_copy(modifier = mod.name) bpy.ops.object.mode_set(mode = 'EDIT') bpy.ops.mesh.select_all(action = 'SELECT') #test the resolution of the stroke #subdivide if necessary linear_density = odcutils.get_linear_density(me, edges, mx, debug) #flatten and space to make my simple curvature more succesful :-) bpy.ops.mesh.looptools_flatten(influence=90, plane='best_fit', restriction='none') bpy.ops.mesh.looptools_space(influence=90, input='selected', interpolation='cubic') bpy.ops.object.mode_set(mode = 'OBJECT') bpy.ops.object.modifier_apply(modifier = trace_obj.modifiers[1].name) #Now we should essentially have a nice, approximately 2D and evenly spaced line #And we will find the sharpest point and save it. verts = trace_data.vertices v_ind = [v.index for v in trace_data.vertices] eds = [e for e in trace_data.edges if e.select] #why would we filter for selection here? ed_vecs = [(verts[e.vertices[1]].co - verts[e.vertices[0]].co) for e in eds] locs = [] curves = [] for i in range(3,len(eds)-3): a1 = ed_vecs[i-1].angle(ed_vecs[i+1]) a2 = ed_vecs[i-2].angle(ed_vecs[i+2]) a3 = ed_vecs[i-3].angle(ed_vecs[i+3]) l1 = ed_vecs[i-1].length + ed_vecs[i+1].length l2 = ed_vecs[i-2].length + ed_vecs[i+2].length l3 = ed_vecs[i-3].length + ed_vecs[i+3].length curve = 1/6 * (3*a1/l1 + 2 * a2/l2 + a3/l3) curves.append(curve) c = max(curves) n = curves.index(c) max_ed = eds[n+3] #need to check this indexing loc = .5 * (verts[max_ed.vertices[0]].co + verts[max_ed.vertices[1]].co) locs.append(loc) bpy.ops.object.mode_set(mode = 'EDIT') bpy.context.scene.cursor_location = locs[0] bpy.ops.transform.resize(value = (0,0,1)) bpy.ops.mesh.looptools_space(influence=100, input='selected', interpolation='cubic') bpy.context.scene.cursor_location = Prep_Center bpy.ops.transform.rotate(value = (2*math.pi/self.resolution), axis = (0,0,1)) bpy.ops.object.mode_set(mode = 'OBJECT') bpy.ops.object.modifier_copy(modifier = mod.name) bpy.ops.object.modifier_apply(modifier = trace_obj.modifiers[1].name) bpy.ops.object.mode_set(mode = 'EDIT') bpy.ops.mesh.looptools_flatten(influence=90, plane='best_fit', restriction='none') bpy.ops.mesh.looptools_space(influence=90, input='selected', interpolation='cubic') bpy.ops.object.mode_set(mode = 'OBJECT') for b in range(1,self.resolution+self.extra): verts = trace_data.vertices eds = [e for e in trace_data.edges if e.select] ed_vecs = [(verts[e.vertices[1]].co - verts[e.vertices[0]].co) for e in eds] curves = [] for i in range(3,len(eds)-3): a1 = ed_vecs[i-1].angle(ed_vecs[i+1]) a2 = ed_vecs[i-2].angle(ed_vecs[i+2]) a3 = ed_vecs[i-3].angle(ed_vecs[i+3]) l1 = ed_vecs[i-1].length + ed_vecs[i+1].length l2 = ed_vecs[i-2].length + ed_vecs[i+2].length l3 = ed_vecs[i-3].length + ed_vecs[i+3].length curve = 1/6 * (3*a1/l1 + 2 * a2/l2 + a3/l3) curves.append(curve) c = max(curves) n = curves.index(c) max_ed = eds[n+3] #need to check this indexing loc = .5 * (verts[max_ed.vertices[0]].co + verts[max_ed.vertices[1]].co) locs.append(loc) bpy.ops.object.mode_set(mode = 'EDIT') bpy.context.scene.cursor_location = locs[b] zscale = self.search/trace_obj.dimensions[2] #if the shrinkwrapping has resulted in contraction or dilation, we want to fix that. bpy.ops.transform.resize(value = (0,0,zscale)) bpy.ops.mesh.looptools_space(influence=100, input='selected', interpolation='cubic') bpy.context.scene.cursor_location = Prep_Center COM = odcutils.get_com(trace_data,v_ind,'') delt = locs[b] - COM bpy.ops.transform.translate(value = (delt[0], delt[1], delt[2])) bpy.ops.transform.rotate(value = (2*math.pi/self.resolution), axis = (0,0,1)) bpy.ops.object.mode_set(mode = 'OBJECT') bpy.ops.object.modifier_copy(modifier = mod.name) bpy.ops.object.modifier_apply(modifier = trace_obj.modifiers[1].name) bpy.ops.object.mode_set(mode = 'EDIT') bpy.ops.mesh.looptools_flatten(influence=90, plane='best_fit', restriction='none') bpy.ops.mesh.looptools_space(influence=90, input='selected', interpolation='cubic') bpy.ops.object.mode_set(mode = 'OBJECT') bpy.ops.object.modifier_copy(modifier = mod.name) bpy.ops.object.modifier_apply(modifier = trace_obj.modifiers[1].name) bpy.ops.object.mode_set(mode = 'EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.remove_doubles(threshold = .025) #this is probably the limit of any scanner accuracy anyway bpy.ops.object.mode_set(mode = 'OBJECT') margin_data = bpy.data.meshes.new(margin) edges = [] for i in range(0,len(locs)-1): edges.append([i,i+1]) edges.append([len(locs)-1,0]) faces = [] margin_data.from_pydata(locs,edges,faces) margin_data.update() Margin = bpy.data.objects.new(margin, margin_data) sce.objects.link(Margin) current_objects = list(bpy.data.objects) bpy.ops.mesh.primitive_uv_sphere_add(size = .1) for ob in sce.objects: if ob not in current_objects: ob.name = margin + "_marker" ob.parent = Margin me = ob.data #me.materials.append(bpy.data.materials['intaglio_material']) Margin.dupli_type = 'VERTS' Margin.parent = Master tooth.margin = margin #put the transform orientation back bpy.context.space_data.transform_orientation = current_transform return {'FINISHED'}
def execute(self, context): tooth = odcutils.tooth_selection( context )[0] #This could theoretically happen to multiple teeth...but not likely sce = bpy.context.scene a = tooth.name prep = tooth.prep_model margin = str(a + "_Margin") Prep = bpy.data.objects[prep] Prep.hide = False master = sce.odc_props.master Master = bpy.data.objects[master] gp_margin = bpy.data.grease_pencil.get(margin + "_tracer") if not gp_margin: self.report( "ERROR", "No grease pencil margin trace mark, please 'Initiate Auto Margin' first" ) return {'CANCELLED'} #Set up the rotation center as the 3d Cursor for A in bpy.context.window.screen.areas: if A.type == 'VIEW_3D': for s in A.spaces: if s.type == 'VIEW_3D': s.pivot_point = 'CURSOR' #set the trasnform orientation to the insertion axis so our z is well definined #this function returns the current transform so we can put it back later current_transform = odcutils.transform_management( tooth, sce, bpy.context.space_data) #Get the prep BBox center for later prep_cent = Vector((0, 0, 0)) for v in Prep.bound_box: prep_cent = prep_cent + Prep.matrix_world * Vector(v) Prep_Center = prep_cent / 8 bpy.ops.gpencil.convert(type='PATH') bpy.ops.object.select_all(action='DESELECT') trace_curve = bpy.data.objects[ margin + "_tracer"] #refactor to test for new object in scene context.scene.objects.active = trace_curve trace_curve.select = True bpy.ops.object.convert(target='MESH') #get our data trace_obj = bpy.context.object trace_data = trace_obj.data #place the intitial shrinkwrap modifier bpy.ops.object.modifier_add(type='SHRINKWRAP') mod = trace_obj.modifiers[0] mod.target = Prep mod.show_in_editmode = True mod.show_on_cage = True bpy.ops.object.modifier_copy(modifier=mod.name) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') #test the resolution of the stroke #subdivide if necessary linear_density = odcutils.get_linear_density(me, edges, mx, debug) #flatten and space to make my simple curvature more succesful :-) bpy.ops.mesh.looptools_flatten(influence=90, plane='best_fit', restriction='none') bpy.ops.mesh.looptools_space(influence=90, input='selected', interpolation='cubic') bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.modifier_apply(modifier=trace_obj.modifiers[1].name) #Now we should essentially have a nice, approximately 2D and evenly spaced line #And we will find the sharpest point and save it. verts = trace_data.vertices v_ind = [v.index for v in trace_data.vertices] eds = [e for e in trace_data.edges if e.select] #why would we filter for selection here? ed_vecs = [(verts[e.vertices[1]].co - verts[e.vertices[0]].co) for e in eds] locs = [] curves = [] for i in range(3, len(eds) - 3): a1 = ed_vecs[i - 1].angle(ed_vecs[i + 1]) a2 = ed_vecs[i - 2].angle(ed_vecs[i + 2]) a3 = ed_vecs[i - 3].angle(ed_vecs[i + 3]) l1 = ed_vecs[i - 1].length + ed_vecs[i + 1].length l2 = ed_vecs[i - 2].length + ed_vecs[i + 2].length l3 = ed_vecs[i - 3].length + ed_vecs[i + 3].length curve = 1 / 6 * (3 * a1 / l1 + 2 * a2 / l2 + a3 / l3) curves.append(curve) c = max(curves) n = curves.index(c) max_ed = eds[n + 3] #need to check this indexing loc = .5 * (verts[max_ed.vertices[0]].co + verts[max_ed.vertices[1]].co) locs.append(loc) bpy.ops.object.mode_set(mode='EDIT') bpy.context.scene.cursor_location = locs[0] bpy.ops.transform.resize(value=(0, 0, 1)) bpy.ops.mesh.looptools_space(influence=100, input='selected', interpolation='cubic') bpy.context.scene.cursor_location = Prep_Center bpy.ops.transform.rotate(value=(2 * math.pi / self.resolution), axis=(0, 0, 1)) bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.modifier_copy(modifier=mod.name) bpy.ops.object.modifier_apply(modifier=trace_obj.modifiers[1].name) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.looptools_flatten(influence=90, plane='best_fit', restriction='none') bpy.ops.mesh.looptools_space(influence=90, input='selected', interpolation='cubic') bpy.ops.object.mode_set(mode='OBJECT') for b in range(1, self.resolution + self.extra): verts = trace_data.vertices eds = [e for e in trace_data.edges if e.select] ed_vecs = [(verts[e.vertices[1]].co - verts[e.vertices[0]].co) for e in eds] curves = [] for i in range(3, len(eds) - 3): a1 = ed_vecs[i - 1].angle(ed_vecs[i + 1]) a2 = ed_vecs[i - 2].angle(ed_vecs[i + 2]) a3 = ed_vecs[i - 3].angle(ed_vecs[i + 3]) l1 = ed_vecs[i - 1].length + ed_vecs[i + 1].length l2 = ed_vecs[i - 2].length + ed_vecs[i + 2].length l3 = ed_vecs[i - 3].length + ed_vecs[i + 3].length curve = 1 / 6 * (3 * a1 / l1 + 2 * a2 / l2 + a3 / l3) curves.append(curve) c = max(curves) n = curves.index(c) max_ed = eds[n + 3] #need to check this indexing loc = .5 * (verts[max_ed.vertices[0]].co + verts[max_ed.vertices[1]].co) locs.append(loc) bpy.ops.object.mode_set(mode='EDIT') bpy.context.scene.cursor_location = locs[b] zscale = self.search / trace_obj.dimensions[ 2] #if the shrinkwrapping has resulted in contraction or dilation, we want to fix that. bpy.ops.transform.resize(value=(0, 0, zscale)) bpy.ops.mesh.looptools_space(influence=100, input='selected', interpolation='cubic') bpy.context.scene.cursor_location = Prep_Center COM = odcutils.get_com(trace_data, v_ind, '') delt = locs[b] - COM bpy.ops.transform.translate(value=(delt[0], delt[1], delt[2])) bpy.ops.transform.rotate(value=(2 * math.pi / self.resolution), axis=(0, 0, 1)) bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.modifier_copy(modifier=mod.name) bpy.ops.object.modifier_apply(modifier=trace_obj.modifiers[1].name) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.looptools_flatten(influence=90, plane='best_fit', restriction='none') bpy.ops.mesh.looptools_space(influence=90, input='selected', interpolation='cubic') bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.modifier_copy(modifier=mod.name) bpy.ops.object.modifier_apply(modifier=trace_obj.modifiers[1].name) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.remove_doubles( threshold=.025 ) #this is probably the limit of any scanner accuracy anyway bpy.ops.object.mode_set(mode='OBJECT') margin_data = bpy.data.meshes.new(margin) edges = [] for i in range(0, len(locs) - 1): edges.append([i, i + 1]) edges.append([len(locs) - 1, 0]) faces = [] margin_data.from_pydata(locs, edges, faces) margin_data.update() Margin = bpy.data.objects.new(margin, margin_data) sce.objects.link(Margin) current_objects = list(bpy.data.objects) bpy.ops.mesh.primitive_uv_sphere_add(size=.1) for ob in sce.objects: if ob not in current_objects: ob.name = margin + "_marker" ob.parent = Margin me = ob.data #me.materials.append(bpy.data.materials['intaglio_material']) Margin.dupli_type = 'VERTS' Margin.parent = Master tooth.margin = margin #put the transform orientation back bpy.context.space_data.transform_orientation = current_transform return {'FINISHED'}
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 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()