Skip to content
Snippets Groups Projects
mesh_bsurfaces.py 195 KiB
Newer Older
  • Learn to ignore specific revisions
  •                     vec_A = points_original[0] - points_original[1]
                        vec_B = points_target[0] - points_target[1]
    
                        # check for zero angles, not sure if it is a great fix
                        if vec_A.length != 0 and vec_B.length != 0:
                            angle = vec_A.angle(vec_B) / pi
                            edge_new_length = (Vector(verts_middle_position_co) - sp[1][i]).length
                        else:
                            angle = 0
                            edge_new_length = 0
    
                        # If after moving the verts to the middle point, the segment doesn't stretch too much
                        if edge_new_length <= loop_segment_dist * 1.5 * \
                           self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor:
    
                            # Avoid joining when the actual loop must be merged with the original mesh
                            if not (self.selection_U_exists and i == 0) and \
                               not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1):
    
                                # Change the coords of both verts to the middle position
    
                                surface_splines_parsed[0][i] = verts_middle_position_co
                                surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
    
            # Delete object with control points and object from grease pencil conversion
    
            bpy.ops.object.delete({"selected_objects": [ob_ctrl_pts]})
    
            bpy.ops.object.delete({"selected_objects": splines_U_objects})
    
            # Generate surface
    
            # Get all verts coords
    
            all_surface_verts_co = []
            for i in range(0, len(surface_splines_parsed)):
                # Get coords of all verts and make a list with them
                for pt_co in surface_splines_parsed[i]:
                    all_surface_verts_co.append(pt_co)
    
            # Define verts for each face
    
            all_surface_faces = []
            for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
                if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
    
                    all_surface_faces.append(
                                [i + 1, i, i + len(surface_splines_parsed[0]),
                                i + len(surface_splines_parsed[0]) + 1]
                                )
            # Build the mesh
    
            surf_me_name = "SURFSKIO_surface"
            me_surf = bpy.data.meshes.new(surf_me_name)
    
            me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
    
            me_surf.update()
    
            ob_surface = bpy.data.objects.new(surf_me_name, me_surf)
    
            bpy.context.collection.objects.link(ob_surface)
    
            # Select all the "unselected but participating" verts, from closed selection
            # or double selections with middle-vertex, for later join with remove doubles
    
            for v_idx in single_unselected_verts:
    
                self.main_object.data.vertices[v_idx].select = True
    
            # Join the new mesh to the main object
    
            ob_surface.select_set(True)
    
            self.main_object.select_set(True)
    
            bpy.context.view_layer.objects.active = self.main_object
    
            bpy.ops.object.join('INVOKE_REGION_WIN')
    
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
            bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
    
            bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
            bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
            return{'FINISHED'}
    
        def execute(self, context):
    
            if bpy.ops.object.mode_set.poll():
                 bpy.ops.object.mode_set(mode='OBJECT')
            
    
            bsurfaces_props = bpy.context.scene.bsurfaces
    
            self.main_object = bsurfaces_props.SURFSK_object_with_retopology
    
            self.main_object.select_set(True)
            bpy.context.view_layer.objects.active = self.main_object
    
            if not self.is_fill_faces:
    
                bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
                                             value='True, False, False')
    
                # Build splines from the "last saved splines".
                last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
                self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
    
                bpy.context.collection.objects.link(self.main_splines)
    
                last_saved_curve.dimensions = "3D"
    
                for sp in self.last_strokes_splines_coords:
                    spline = self.main_splines.data.splines.new('BEZIER')
    
                    # less one because one point is added when the spline is created
                    spline.bezier_points.add(len(sp) - 1)
    
                    for p in range(0, len(sp)):
                        spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
    
                #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
                bpy.ops.object.mode_set(mode='OBJECT')
    
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                self.main_splines.select_set(True)
    
                bpy.context.view_layer.objects.active = self.main_splines
    
                #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
                bpy.ops.object.mode_set(mode='EDIT')
    
                bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
    
                # Important to make it vector first and then automatic, otherwise the
                # tips handles get too big and distort the shrinkwrap results later
                bpy.ops.curve.handle_type_set(type='VECTOR')
    
                bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
                bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                self.main_splines.name = "SURFSKIO_temp_strokes"
    
                if self.is_crosshatch:
                    strokes_for_crosshatch = True
                    strokes_for_rectangular_surface = False
                else:
                    strokes_for_rectangular_surface = True
                    strokes_for_crosshatch = False
    
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                self.main_object.select_set(True)
    
                bpy.context.view_layer.objects.active = self.main_object
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                if strokes_for_rectangular_surface:
                    self.rectangular_surface()
                elif strokes_for_crosshatch:
                    self.crosshatch_surface_execute()
    
                # Delete main splines
    
                #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
                bpy.ops.object.mode_set(mode='OBJECT')
    
                if self.keep_strokes:
                    self.main_splines.name = "keep_strokes"
                    self.main_splines.data.bevel_depth = 0.001
                    if "keep_strokes_material" in bpy.data.materials :
                        self.main_splines.data.materials.append(bpy.data.materials["keep_strokes_material"])
                    else:
                        mat = bpy.data.materials.new("keep_strokes_material")
                        mat.diffuse_color = (1, 0, 0, 0)
                        mat.specular_color = (1, 0, 0)
                        mat.specular_intensity = 0.0
                        mat.roughness = 0.0
                        self.main_splines.data.materials.append(mat)
                else:
                    bpy.ops.object.delete({"selected_objects": [self.main_splines]})
    
                if self.strokes_type == "GP_STROKES" and not self.stopping_errors:
    
                    try:
                        bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers.active.clear()
                    except:
                        pass
    
                if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors:
    
                    try:
                        bpy.data.grease_pencils[0].layers.active.clear()
                    except:
                        pass
    
    
                bsurfaces_props.SURFSK_edges_U = self.edges_U
                bsurfaces_props.SURFSK_edges_V = self.edges_V
                bsurfaces_props.SURFSK_cyclic_cross = self.cyclic_cross
                bsurfaces_props.SURFSK_cyclic_follow = self.cyclic_follow
                bsurfaces_props.SURFSK_automatic_join = self.automatic_join
                bsurfaces_props.SURFSK_loops_on_strokes = self.loops_on_strokes
                bsurfaces_props.SURFSK_keep_strokes = self.keep_strokes
                
    
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                self.main_object.select_set(True)
    
                bpy.context.view_layer.objects.active = self.main_object
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
            return{'FINISHED'}
    
        def invoke(self, context, event):
    
            if bpy.ops.object.mode_set.poll():
                 bpy.ops.object.mode_set(mode='OBJECT')
    
            bsurfaces_props = bpy.context.scene.bsurfaces
            self.cyclic_cross = bsurfaces_props.SURFSK_cyclic_cross
            self.cyclic_follow = bsurfaces_props.SURFSK_cyclic_follow
            self.automatic_join = bsurfaces_props.SURFSK_automatic_join
            self.loops_on_strokes = bsurfaces_props.SURFSK_loops_on_strokes
            self.keep_strokes = bsurfaces_props.SURFSK_keep_strokes
    
            self.main_object = bsurfaces_props.SURFSK_object_with_retopology
    
                self.main_object.select_set(True)
    
            except:
                self.report({'WARNING'}, "Specify the name of the object with retopology")
                return{"CANCELLED"}
    
            bpy.context.view_layer.objects.active = self.main_object
    
    
            self.main_object_selected_verts_count = len([v for v in self.main_object.data.vertices if v.select])
    
    
            bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
                                         value='True, False, False')
    
            #if self.loops_on_strokes:
            #    self.edges_V = 1
            #else:
            #    self.edges_V = bsurfaces_props.SURFSK_edges_V
    
            self.edges_V = bsurfaces_props.SURFSK_edges_V
    
            self.is_fill_faces = False
            self.stopping_errors = False
            self.last_strokes_splines_coords = []
    
            # Determine the type of the strokes
    
            self.strokes_type = get_strokes_type(context)
    
            # Check if it will be used grease pencil strokes or curves
            # If there are strokes to be used
    
            if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "GP_ANNOTATION":
    
                if self.strokes_type == "GP_STROKES":
    
                    # Convert grease pencil strokes to curve
    
                    gp = bsurfaces_props.SURFSK_object_with_strokes
    
                    #bpy.ops.gpencil.convert(type='CURVE', use_link_strokes=False)
    
                    self.original_curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
    
                    # XXX gpencil.convert now keep org object as active/selected, *not* newly created curve!
                    # XXX This is far from perfect, but should work in most cases...
                    # self.original_curve = bpy.context.object
                    gplayer_prefix_translated = bpy.app.translations.pgettext_data('GP_Layer')
                    for ob in bpy.context.selected_objects:
                        if ob != bpy.context.view_layer.objects.active and \
                           ob.name.startswith((gplayer_prefix_translated, 'GP_Layer')):
                            self.original_curve = ob
                    self.using_external_curves = False
                    
                elif self.strokes_type == "GP_ANNOTATION":
                    # Convert grease pencil strokes to curve
                    gp = bpy.data.grease_pencils["Annotations"]
                    #bpy.ops.gpencil.convert(type='CURVE', use_link_strokes=False)
    
                    self.original_curve = conver_gpencil_to_curve(self, context, gp, 'Annotation')
    
                    # XXX gpencil.convert now keep org object as active/selected, *not* newly created curve!
                    # XXX This is far from perfect, but should work in most cases...
    
                    # self.original_curve = bpy.context.object
    
                    gplayer_prefix_translated = bpy.app.translations.pgettext_data('GP_Layer')
    
                        if ob != bpy.context.view_layer.objects.active and \
    
                           ob.name.startswith((gplayer_prefix_translated, 'GP_Layer')):
    
                    self.using_external_curves = False
    
                elif self.strokes_type == "EXTERNAL_CURVE":
    
                    self.original_curve = bsurfaces_props.SURFSK_object_with_strokes
    
                    self.using_external_curves = True
    
                    bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                # Make sure there are no objects left from erroneous
                # executions of this operator, with the reserved names used here
    
                for o in bpy.data.objects:
                    if o.name.find("SURFSKIO_") != -1:
    
                        bpy.ops.object.delete({"selected_objects": [o]})
    
                #bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                self.original_curve.select_set(True)
    
                bpy.context.view_layer.objects.active = self.original_curve
    
                bpy.ops.object.duplicate('INVOKE_REGION_WIN')
    
                self.temporary_curve = bpy.context.view_layer.objects.active
    
                # Deselect all points of the curve
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
                bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                # Delete splines with only a single isolated point
    
                for i in range(len(self.temporary_curve.data.splines)):
                    sp = self.temporary_curve.data.splines[i]
    
                    if len(sp.bezier_points) == 1:
                        sp.bezier_points[0].select_control_point = True
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                bpy.ops.curve.delete(type='VERT')
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                self.temporary_curve.select_set(True)
    
                bpy.context.view_layer.objects.active = self.temporary_curve
    
                # Set a minimum number of points for crosshatch
    
                minimum_points_num = 15
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                # Check if the number of points of each curve has at least the number of points
                # of minimum_points_num, which is a bit more than the face-loops limit.
    
                # If not, subdivide to reach at least that number of points
    
                for i in range(len(self.temporary_curve.data.splines)):
                    sp = self.temporary_curve.data.splines[i]
    
                    if len(sp.bezier_points) < minimum_points_num:
                        for bp in sp.bezier_points:
                            bp.select_control_point = True
    
                        if (len(sp.bezier_points) - 1) != 0:
    
                            # Formula to get the number of cuts that will make a curve
                            # of N number of points have near to "minimum_points_num"
                            # points, when subdividing with this number of cuts
                            subdivide_cuts = int(
                                        (minimum_points_num - len(sp.bezier_points)) /
                                        (len(sp.bezier_points) - 1)
                                        ) + 1
    
                        else:
                            subdivide_cuts = 0
    
                        bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
    
                        bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                # Detect if the strokes are a crosshatch and do it if it is
    
                self.crosshatch_surface_invoke(self.temporary_curve)
    
                if not self.is_crosshatch:
    
                    bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                    self.temporary_curve.select_set(True)
    
                    bpy.context.view_layer.objects.active = self.temporary_curve
    
                    bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                    # Set a minimum number of points for rectangular surfaces
    
                    minimum_points_num = 60
    
                    # Check if the number of points of each curve has at least the number of points
                    # of minimum_points_num, which is a bit more than the face-loops limit.
    
                    # If not, subdivide to reach at least that number of points
    
                    for i in range(len(self.temporary_curve.data.splines)):
                        sp = self.temporary_curve.data.splines[i]
    
                        if len(sp.bezier_points) < minimum_points_num:
                            for bp in sp.bezier_points:
                                bp.select_control_point = True
    
                            if (len(sp.bezier_points) - 1) != 0:
    
                                # Formula to get the number of cuts that will make a curve of
                                # N number of points have near to "minimum_points_num" points,
                                # when subdividing with this number of cuts
                                subdivide_cuts = int(
                                            (minimum_points_num - len(sp.bezier_points)) /
                                            (len(sp.bezier_points) - 1)
                                            ) + 1
    
                            else:
                                subdivide_cuts = 0
    
                            bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
    
                            bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                    bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                # Save coordinates of the actual strokes (as the "last saved splines")
    
                for sp_idx in range(len(self.temporary_curve.data.splines)):
                    self.last_strokes_splines_coords.append([])
                    for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
    
                        coords = self.temporary_curve.matrix_world @ \
                                 self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
    
                        self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
    
                # Check for cyclic splines, put the first and last points in the middle of their actual positions
    
                for sp_idx in range(len(self.temporary_curve.data.splines)):
    
                    if self.temporary_curve.data.splines[sp_idx].use_cyclic_u is True:
    
                        first_p_co = self.last_strokes_splines_coords[sp_idx][0]
    
                        last_p_co = self.last_strokes_splines_coords[sp_idx][
                                                                len(self.last_strokes_splines_coords[sp_idx]) - 1
                                                                ]
                        target_co = [
                                (first_p_co[0] + last_p_co[0]) / 2,
                                (first_p_co[1] + last_p_co[1]) / 2,
                                (first_p_co[2] + last_p_co[2]) / 2
                                ]
    
                        self.last_strokes_splines_coords[sp_idx][0] = target_co
    
                        self.last_strokes_splines_coords[sp_idx][
                                                                len(self.last_strokes_splines_coords[sp_idx]) - 1
                                                                ] = target_co
    
                tuple(self.last_strokes_splines_coords)
    
                # Estimation of the average length of the segments between
                # each point of the grease pencil strokes.
                # Will be useful to determine whether a curve should be made "Cyclic"
    
                segments_lengths_sum = 0
                segments_count = 0
                random_spline = self.temporary_curve.data.splines[0].bezier_points
                for i in range(0, len(random_spline)):
                    if i != 0 and len(random_spline) - 1 >= i:
                        segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
                        segments_count += 1
    
                self.average_gp_segment_length = segments_lengths_sum / segments_count
    
                # Delete temporary strokes curve object
    
                bpy.ops.object.delete({"selected_objects": [self.temporary_curve]})
    
                # If "Keep strokes" option is not active, delete original strokes curve object
    
                if not self.stopping_errors or self.is_crosshatch:
    
                    bpy.ops.object.delete({"selected_objects": [self.original_curve]})
    
                # Delete grease pencil strokes
    
                if self.strokes_type == "GP_STROKES" and not self.stopping_errors:
    
                    try:
                        bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers.active.clear()
                    except:
                        pass
    
                if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors:
    
                    try:
                        bpy.data.grease_pencils[0].layers.active.clear()
                    except:
                        pass
    
                
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
                self.main_object.select_set(True)
                bpy.context.view_layer.objects.active = self.main_object
                
                # Set again since "execute()" will turn it again to its initial value
                self.execute(context)
    
                if not self.stopping_errors:
                    return {"FINISHED"}
                else:
                    return{"CANCELLED"}
    
            elif self.strokes_type == "SELECTION_ALONE":
                self.is_fill_faces = True
                created_faces_count = self.fill_with_faces(self.main_object)
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                if created_faces_count == 0:
    
                    self.report({'WARNING'}, "There aren't any strokes attached to the object")
    
                    return {"CANCELLED"}
                else:
                    return {"FINISHED"}
    
            if self.strokes_type == "EXTERNAL_NO_CURVE":
    
                self.report({'WARNING'}, "The secondary object is not a Curve.")
                return{"CANCELLED"}
    
            elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
                self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
                return{"CANCELLED"}
    
            elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or \
                 self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
    
    
                self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
                return{"CANCELLED"}
    
            elif self.strokes_type == "NO_STROKES":
    
                self.report({'WARNING'}, "There aren't any strokes attached to the object")
    
                return{"CANCELLED"}
    
            elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
                self.report({'WARNING'}, "All splines must be Bezier.")
                return{"CANCELLED"}
    
            else:
                return{"CANCELLED"}
    
    
    # Edit strokes operator
    class GPENCIL_OT_SURFSK_init(Operator):
        bl_idname = "gpencil.surfsk_init"
        bl_label = "Bsurfaces initialize"
    
        bl_description = "Bsurfaces initialize"
    
    
        active_object: PointerProperty(type=bpy.types.Object)
    
            if bpy.ops.object.mode_set.poll():
                 bpy.ops.object.mode_set(mode='OBJECT')
    
            if bs.SURFSK_object_with_retopology == None:
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
                mesh = bpy.data.meshes.new('BSurfaceMesh')
                mesh_object = object_utils.object_data_add(context, mesh, operator=None)
                mesh_object.select_set(True)
                mesh_object.show_all_edges = True
                mesh_object.show_in_front = True
                mesh_object.display_type = 'SOLID'
                mesh_object.show_wire = True
                bpy.context.view_layer.objects.active = mesh_object
                bpy.ops.object.modifier_add(type='SHRINKWRAP')
                modifier = mesh_object.modifiers["Shrinkwrap"]
                if self.active_object is not None:
                    modifier.target = self.active_object
                    modifier.wrap_method = 'TARGET_PROJECT'
                    modifier.wrap_mode = 'OUTSIDE_SURFACE'
                    #modifier.offset = 0.05
                
                bpy.context.scene.bsurfaces.SURFSK_object_with_retopology = mesh_object
            
    
            if context.scene.bsurfaces.SURFSK_guide == 'GPencil' and bs.SURFSK_object_with_strokes == None:
    
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                bpy.ops.object.gpencil_add(radius=1.0, align='WORLD', location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), type='EMPTY')
    
                bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d = 'SURFACE'
                gpencil_object = bpy.context.scene.objects[bpy.context.scene.objects[-1].name]
                gpencil_object.select_set(True)
                bpy.context.view_layer.objects.active = gpencil_object
                bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
                bpy.context.scene.bsurfaces.SURFSK_object_with_strokes = gpencil_object
    
                gpencil_object.data.stroke_depth_order = '3D'
    
            if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
    
                bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
                bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
    
            return {"FINISHED"}
    
        def invoke(self, context, event):
            if bpy.context.active_object:
                self.active_object = bpy.context.active_object
            else:
                self.active_object = None
            
            self.execute(context)
    
            return {"FINISHED"}
    
            
    # Edit strokes operator
    class GPENCIL_OT_SURFSK_add_modifiers(Operator):
        bl_idname = "gpencil.surfsk_add_modifiers"
        bl_label = "Add Mirror and others modifiers"
        bl_description = "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify "
    
        active_object: PointerProperty(type=bpy.types.Object)
    
        def execute(self, context):
        
            bs = bpy.context.scene.bsurfaces
        
            if bpy.ops.object.mode_set.poll():
                 bpy.ops.object.mode_set(mode='OBJECT')
            
            if bs.SURFSK_object_with_retopology == None:
                self.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
            else:
                mesh_object = bs.SURFSK_object_with_retopology
                
                try:
                    mesh_object.select_set(True)
                except:
                    self.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
                    return {"CANCEL"}
                
                bpy.context.view_layer.objects.active = mesh_object
                
                try:
                    shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
                    if self.active_object is not None and self.active_object != mesh_object:
                        shrinkwrap.target = self.active_object
                        shrinkwrap.wrap_method = 'TARGET_PROJECT'
                        shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
                except:
                    bpy.ops.object.modifier_add(type='SHRINKWRAP')
                    shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
                    if self.active_object is not None and self.active_object != mesh_object:
                        shrinkwrap.target = self.active_object
                        shrinkwrap.wrap_method = 'TARGET_PROJECT'
                        shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
                
                try:
                    mirror = mesh_object.modifiers["Mirror"]
                    mirror.use_clip = True
                except:
                    bpy.ops.object.modifier_add(type='MIRROR')
                    mirror = mesh_object.modifiers["Mirror"]
                    mirror.use_clip = True
                
                try:
                    subsurf = mesh_object.modifiers["Subdivision"]
                except:
                    bpy.ops.object.modifier_add(type='SUBSURF')
                    subsurf = mesh_object.modifiers["Subdivision"]
                
                try:
                    solidify = mesh_object.modifiers["Solidify"]
                    solidify.thickness = 0.01
                except:
                    bpy.ops.object.modifier_add(type='SOLIDIFY')
                    solidify = mesh_object.modifiers["Solidify"]
                    solidify.thickness = 0.01
    
            return {"FINISHED"}
    
        def invoke(self, context, event):
            if bpy.context.active_object:
                self.active_object = bpy.context.active_object
            else:
                self.active_object = None
            
            self.execute(context)
    
            return {"FINISHED"}
    
    
    # Edit surface operator
    class GPENCIL_OT_SURFSK_edit_surface(Operator):
        bl_idname = "gpencil.surfsk_edit_surface"
        bl_label = "Bsurfaces edit surface"
        bl_description = "Edit surface mesh"
    
        def execute(self, context):
            bpy.context.scene.bsurfaces.SURFSK_object_with_retopology.select_set(True)
            bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_object_with_retopology
            bpy.ops.object.mode_set(mode='EDIT')
            
        def invoke(self, context, event):
            try:
                bpy.context.scene.bsurfaces.SURFSK_object_with_retopology.select_set(True)
            except:
                self.report({'WARNING'}, "Specify the name of the object with retopology")
                return{"CANCELLED"}
            
            self.execute(context)
    
            return {"FINISHED"}
    
    class GPENCIL_OT_SURFSK_add_strokes(Operator):
        bl_idname = "gpencil.surfsk_add_strokes"
        bl_label = "Bsurfaces add strokes"
        bl_description = "Add the grease pencil strokes"
    
        def execute(self, context):
            # Determine the type of the strokes
    
            self.strokes_type = get_strokes_type(context)
    
            # Check if strokes are grease pencil strokes or a curves object
            selected_objs = bpy.context.selected_objects
            if self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
                for ob in selected_objs:
                    if ob != bpy.context.view_layer.objects.active:
                        curve_ob = ob
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
    
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                curve_ob.select_set(True)
                bpy.context.view_layer.objects.active = curve_ob
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.select_set(True)
                bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_object_with_strokes
    
                bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
    
    
                return{"FINISHED"}
    
    
        def invoke(self, context, event):
    
            try:
                bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.select_set(True)
            except:
                self.report({'WARNING'}, "Specify the name of the object with strokes")
                return{"CANCELLED"}
            
    
            self.execute(context)
    
            return {"FINISHED"}
    
    
    # Edit strokes operator
    class GPENCIL_OT_SURFSK_edit_strokes(Operator):
    
        bl_idname = "gpencil.surfsk_edit_strokes"
        bl_label = "Bsurfaces edit strokes"
    
        bl_description = "Edit the grease pencil strokes or curves used"
    
        def execute(self, context):
    
            # Determine the type of the strokes
    
            self.strokes_type = get_strokes_type(context)
    
            # Check if strokes are grease pencil strokes or a curves object
    
            selected_objs = bpy.context.selected_objects
            if self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
                for ob in selected_objs:
    
                    if ob != bpy.context.view_layer.objects.active:
    
                        curve_ob = ob
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                curve_ob.select_set(True)
    
                bpy.context.view_layer.objects.active = curve_ob
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            elif self.strokes_type == "GP_STROKES" or self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION":
    
                # Convert grease pencil strokes to curve
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                #bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
    
                gp = bpy.context.scene.bsurfaces.SURFSK_object_with_strokes
    
                conver_gpencil_to_curve(self, context, gp, 'GPensil')
    
                for ob in bpy.context.selected_objects:
    
                        if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
    
                ob_gp_strokes = bpy.context.object
    
                # Delete grease pencil strokes
    
                try:
                    bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers.active.clear()
                except:
                    pass
    
                # Clean up curves
    
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                ob_gp_strokes.select_set(True)
    
                bpy.context.view_layer.objects.active = ob_gp_strokes
    
                curve_crv = ob_gp_strokes.data
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
                bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type="BEZIER")
                bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type="AUTOMATIC")
    
                #curve_crv.show_handles = False
                #curve_crv.show_normal_face = False
    
            elif self.strokes_type == "EXTERNAL_NO_CURVE":
                self.report({'WARNING'}, "The secondary object is not a Curve.")
                return{"CANCELLED"}
    
            elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
                self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
                return{"CANCELLED"}
    
            elif self.strokes_type == "NO_STROKES" or self.strokes_type == "SELECTION_ALONE":
    
                self.report({'WARNING'}, "There aren't any strokes attached to the object")
    
                return{"CANCELLED"}
    
            else:
                return{"CANCELLED"}
    
        def invoke(self, context, event):
    
            try:
               bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.select_set(True)
            except:
                self.report({'WARNING'}, "Specify the name of the object with strokes")
                return{"CANCELLED"}
    
    
            self.execute(context)
    
            return {"FINISHED"}
    
    
    # Add annotation
    class GPENCIL_OT_SURFSK_add_annotation(Operator):
        bl_idname = "gpencil.surfsk_add_annotation"
        bl_label = "Bsurfaces add annotation"
        bl_description = "Add annotation"
    
        def execute(self, context):
            bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
            bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
    
            return{"FINISHED"}
    
        def invoke(self, context, event):
            
            self.execute(context)
    
            return {"FINISHED"}
    
    class CURVE_OT_SURFSK_reorder_splines(Operator):
    
        bl_idname = "curve.surfsk_reorder_splines"
        bl_label = "Bsurfaces reorder splines"
    
        bl_description = "Defines the order of the splines by using grease pencil strokes"
    
        bl_options = {'REGISTER', 'UNDO'}
    
        def execute(self, context):
            objects_to_delete = []
    
            # Convert grease pencil strokes to curve.
    
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
            bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
            for ob in bpy.context.selected_objects:
    
                if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
    
            # GP_strokes_curve = bpy.context.object
    
            objects_to_delete.append(GP_strokes_curve)
    
            bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
            GP_strokes_curve.select_set(True)
    
            bpy.context.view_layer.objects.active = GP_strokes_curve
    
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
    
            bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=100)
    
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
            bpy.ops.object.duplicate('INVOKE_REGION_WIN')
            GP_strokes_mesh = bpy.context.object
            objects_to_delete.append(GP_strokes_mesh)
    
            GP_strokes_mesh.data.resolution_u = 1
            bpy.ops.object.convert(target='MESH', keep_original=False)
    
            bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
            self.main_curve.select_set(True)
    
            bpy.context.view_layer.objects.active = self.main_curve
    
            bpy.ops.object.duplicate('INVOKE_REGION_WIN')
            curves_duplicate_1 = bpy.context.object
            objects_to_delete.append(curves_duplicate_1)
    
            minimum_points_num = 500
    
            # Some iterations since the subdivision operator
            # has a limit of 100 subdivisions per iteration
            for x in range(round(minimum_points_num / 100)):
                # Check if the number of points of each curve has at least the number of points
    
                # of minimum_points_num. If not, subdivide to reach at least that number of points
    
                for i in range(len(curves_duplicate_1.data.splines)):
                    sp = curves_duplicate_1.data.splines[i]
    
                    if len(sp.bezier_points) < minimum_points_num:
                        for bp in sp.bezier_points:
                            bp.select_control_point = True
    
                        if (len(sp.bezier_points) - 1) != 0:
    
                            # Formula to get the number of cuts that will make a curve of N
                            # number of points have near to "minimum_points_num" points,
                            # when subdividing with this number of cuts
                            subdivide_cuts = int(
                                    (minimum_points_num - len(sp.bezier_points)) /
                                    (len(sp.bezier_points) - 1)
                                    ) + 1
    
                        else:
                            subdivide_cuts = 0
    
                        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                        bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
    
                        bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
                        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
            bpy.ops.object.duplicate('INVOKE_REGION_WIN')
            curves_duplicate_2 = bpy.context.object
            objects_to_delete.append(curves_duplicate_2)
    
            # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
    
            bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
            curves_duplicate_2.select_set(True)
    
            bpy.context.view_layer.objects.active = curves_duplicate_2
    
            bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
            curves_duplicate_2.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX"
            curves_duplicate_2.modifiers["Shrinkwrap"].target = GP_strokes_mesh
            bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier='Shrinkwrap')
    
            # Get the distance of each vert from its original position to its position with Shrinkwrap
    
            nearest_points_coords = {}
            for st_idx in range(len(curves_duplicate_1.data.splines)):
                for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
    
                    bp_1_co = curves_duplicate_1.matrix_world @ \
    
                              curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
    
    
                    bp_2_co = curves_duplicate_2.matrix_world @ \
    
                              curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
    
                    if bp_idx == 0:
                        shortest_dist = (bp_1_co - bp_2_co).length
    
                        nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
                                                         "%.4f" % bp_2_co[1],
                                                         "%.4f" % bp_2_co[2])
    
                    dist = (bp_1_co - bp_2_co).length
    
                    if dist < shortest_dist:
    
                        nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
                                                         "%.4f" % bp_2_co[1],
                                                         "%.4f" % bp_2_co[2])
    
                        shortest_dist = dist
    
            # Get all coords of GP strokes points, for comparison
    
            GP_strokes_coords = []
            for st_idx in range(len(GP_strokes_curve.data.splines)):
    
                GP_strokes_coords.append(
                        [("%.4f" % x if "%.4f" % x != "-0.00" else "0.00",
                        "%.4f" % y if "%.4f" % y != "-0.00" else "0.00",
                        "%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for
                        x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]]
                        )
    
            # Check the point of the GP strokes with the same coords as
            # the nearest points of the curves (with shrinkwrap)
    
            # Dictionary with GP stroke index as index, and a list as value.
            # The list has as index the point index of the GP stroke
            # nearest to the spline, and as value the spline index
            GP_connection_points = {}
    
            for gp_st_idx in range(len(GP_strokes_coords)):
                GPvert_spline_relationship = {}
    
                for splines_st_idx in range(len(nearest_points_coords)):
                    if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
    
                        GPvert_spline_relationship[
                            GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])
                            ] = splines_st_idx
    
                GP_connection_points[gp_st_idx] = GPvert_spline_relationship
    
            # Get the splines new order
    
            splines_new_order = []
            for i in GP_connection_points:
    
                dict_keys = sorted(GP_connection_points[i].keys())  # Sort dictionaries by key
    
                for k in dict_keys:
                    splines_new_order.append(GP_connection_points[i][k])
    
            curve_original_name = self.main_curve.name
    
            bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
            self.main_curve.select_set(True)
    
            bpy.context.view_layer.objects.active = self.main_curve
    
            self.main_curve.name = "SURFSKIO_CRV_ORD"
    
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
            for sp_idx in range(len(self.main_curve.data.splines)):
                self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
                bpy.ops.curve.separate('EXEC_REGION_WIN')
    
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
            # Get the names of the separated splines objects in the original order
    
            splines_unordered = {}
            for o in bpy.data.objects:
                if o.name.find("SURFSKIO_CRV_ORD") != -1:
                    spline_order_string = o.name.partition(".")[2]
    
                    if spline_order_string != "" and int(spline_order_string) > 0:
                        spline_order_index = int(spline_order_string) - 1
                        splines_unordered[spline_order_index] = o.name
    
            # Join all splines objects in final order
    
            for order_idx in splines_new_order:
    
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
                bpy.data.objects[splines_unordered[order_idx]].select_set(True)
    
                bpy.data.objects["SURFSKIO_CRV_ORD"].select_set(True)
    
                bpy.context.view_layer.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
    
                bpy.ops.object.join('INVOKE_REGION_WIN')
    
            # Go back to the original name of the curves object.
    
            bpy.context.object.name = curve_original_name
    
            # Delete all unused objects
    
            bpy.ops.object.delete({"selected_objects": objects_to_delete})
    
            bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
            bpy.data.objects[curve_original_name].select_set(True)
    
            bpy.context.view_layer.objects.active = bpy.data.objects[curve_original_name]
    
    Eclectiel L's avatar
    Eclectiel L committed
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
    
            bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
    
            try:
                bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers.active.clear()
            except:
                pass
            
    
            return {"FINISHED"}
    
        def invoke(self, context, event):
    
            self.main_curve = bpy.context.object
            there_are_GP_strokes = False
    
                # Get the active grease pencil layer
    
                strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
    
                if strokes_num > 0:
                    there_are_GP_strokes = True
            except:
                pass
    
            if there_are_GP_strokes:
                self.execute(context)
    
                self.report({'INFO'}, "Splines have been reordered")