Skip to content
Snippets Groups Projects
mesh_bsurfaces.py 199 KiB
Newer Older
# SPDX-License-Identifier: GPL-2.0-or-later
Eclectiel L's avatar
Eclectiel L committed
    "name": "Bsurfaces GPL Edition",
    "author": "Eclectiel, Vladimir Spivak (cwolf3d)",
    "blender": (2, 80, 0),
    "location": "View3D EditMode > Sidebar > Edit Tab",
    "description": "Modeling and retopology tool",
    "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
import bmesh
from bpy_extras import object_utils
import operator
from mathutils import Matrix, Vector
from mathutils.geometry import (
        intersect_line_line,
        intersect_point_line,
        )
from math import (
        degrees,
        pi,
        sqrt,
        )
from bpy.props import (
        BoolProperty,
        FloatProperty,
        IntProperty,
        StringProperty,
        PointerProperty,
        )
from bpy.types import (
        Operator,
        Panel,
        PropertyGroup,
        AddonPreferences,
        )

# ----------------------------
global_mesh_object = ""
global_gpencil_object = ""
global_curve_object = ""
# ----------------------------
#  Panels
class VIEW3D_PT_tools_SURFSK_mesh(Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
Eclectiel L's avatar
Eclectiel L committed
    bl_label = "Bsurfaces"
    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        row = layout.row()
        row.separator()
        col.operator("mesh.surfsk_init", text="Initialize (Add BSurface mesh)")
        col.operator("mesh.surfsk_add_modifiers", text="Add Mirror and others modifiers")

        col.prop(bs, "SURFSK_mesh", text="")
        if bs.SURFSK_mesh != None:
            try: mesh_object = bs.SURFSK_mesh
            except: pass
            try: col.prop(mesh_object.data.materials[0], "diffuse_color")
            except: pass
            try:
                shrinkwrap = next(mod for mod in mesh_object.modifiers
                                  if mod.type == 'SHRINKWRAP')
                col.prop(shrinkwrap, "offset")
            except:
                pass
            try: col.prop(mesh_object, "show_in_front")
            except: pass
            try: col.prop(bs, "SURFSK_shade_smooth")
            except: pass
            try: col.prop(mesh_object, "show_wire")
            except: pass
        col.row().prop(bs, "SURFSK_guide", expand=True)
        if bs.SURFSK_guide == 'GPencil':
            col.prop(bs, "SURFSK_gpencil", text="")
        if bs.SURFSK_guide == 'Curve':
            col.prop(bs, "SURFSK_curve", text="")
        col.operator("mesh.surfsk_add_surface", text="Add Surface")
           col.operator("gpencil.surfsk_add_strokes", text="Add Strokes")
           col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
           col.operator("gpencil.surfsk_strokes_to_curves", text="Strokes to curves")
           col.operator("gpencil.surfsk_add_annotation", text="Add Annotation")
           col.separator()
           col.operator("gpencil.surfsk_annotations_to_curves", text="Annotation to curves")
        col.label(text="Initial settings:")
        col.prop(bs, "SURFSK_edges_U")
        col.prop(bs, "SURFSK_edges_V")
        col.prop(bs, "SURFSK_cyclic_cross")
        col.prop(bs, "SURFSK_cyclic_follow")
        col.prop(bs, "SURFSK_loops_on_strokes")
        col.prop(bs, "SURFSK_automatic_join")
        col.prop(bs, "SURFSK_keep_strokes")
class VIEW3D_PT_tools_SURFSK_curve(Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_context = "curve_edit"
    bl_label = "Bsurfaces"
    @classmethod
    def poll(cls, context):
        return context.active_object
    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        row = layout.row()
        row.separator()
        col.operator("curve.surfsk_first_points", text="Set First Points")
        col.operator("curve.switch_direction", text="Switch Direction")
        col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
# ----------------------------
# Returns the type of strokes used
def get_strokes_type(context):
    strokes_type = "NO_STROKES"
    strokes_num = 0
    # Check if they are annotation
    if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
            strokes = bpy.context.annotation_data.layers.active.active_frame.strokes

            if strokes_num > 0:
               strokes_type = "GP_ANNOTATION"
        except:
            strokes_type = "NO_STROKES"
    # Check if they are grease pencil
    if context.scene.bsurfaces.SURFSK_guide == 'GPencil':
        try:
            global global_gpencil_object
            gpencil = bpy.data.objects[global_gpencil_object]
            strokes = gpencil.data.layers.active.active_frame.strokes
            strokes_num = len(strokes)
            if strokes_num > 0:
               strokes_type = "GP_STROKES"
        except:
            strokes_type = "NO_STROKES"
    # Check if they are curves, if there aren't grease pencil strokes
    if context.scene.bsurfaces.SURFSK_guide == 'Curve':
            global global_curve_object
            ob = bpy.data.objects[global_curve_object]
            if ob.type == "CURVE":
                strokes_type = "EXTERNAL_CURVE"
                strokes_num = len(ob.data.splines)
                # Check if there is any non-bezier spline
                for i in range(len(ob.data.splines)):
                    if ob.data.splines[i].type != "BEZIER":
                        strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
                        break
            else:
                strokes_type = "EXTERNAL_NO_CURVE"
            strokes_type = "NO_STROKES"
    # Check if they are mesh
    try:
        global global_mesh_object
        self.main_object = bpy.data.objects[global_mesh_object]
        total_vert_sel = len([v for v in self.main_object.data.vertices if v.select])
        # Check if there is a single stroke without any selection in the object
        if strokes_num == 1 and total_vert_sel == 0:
            if strokes_type == "EXTERNAL_CURVE":
                strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
            elif strokes_type == "GP_STROKES":
                strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"

        if strokes_num == 0 and total_vert_sel > 0:
            strokes_type = "SELECTION_ALONE"
    except:
        pass
    return strokes_type

# ----------------------------
# Surface generator operator
class MESH_OT_SURFSK_add_surface(Operator):
    bl_idname = "mesh.surfsk_add_surface"
Eclectiel L's avatar
Eclectiel L committed
    bl_label = "Bsurfaces add surface"
    bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
Eclectiel L's avatar
Eclectiel L committed
    bl_options = {'REGISTER', 'UNDO'}
    is_fill_faces: BoolProperty(
                    )
    selection_U_exists: BoolProperty(
                    default=False
                    )
    selection_V_exists: BoolProperty(
                    default=False
                    )
    selection_U2_exists: BoolProperty(
                    default=False
                    )
    selection_V2_exists: BoolProperty(
                    default=False
                    )
    selection_V_is_closed: BoolProperty(
                    default=False
                    )
    selection_U_is_closed: BoolProperty(
                    default=False
                    )
    selection_V2_is_closed: BoolProperty(
                    default=False
                    )
    selection_U2_is_closed: BoolProperty(
                    default=False
                    )
    edges_U: IntProperty(
                    name="Cross",
                    description="Number of face-loops crossing the strokes",
                    default=1,
                    min=1,
                    max=200
                    )
    edges_V: IntProperty(
                    name="Follow",
                    description="Number of face-loops following the strokes",
                    default=1,
                    min=1,
                    max=200
                    )
    cyclic_cross: BoolProperty(
                    name="Cyclic Cross",
                    description="Make cyclic the face-loops crossing the strokes",
                    default=False
                    )
    cyclic_follow: BoolProperty(
                    name="Cyclic Follow",
                    description="Make cyclic the face-loops following the strokes",
                    default=False
                    )
    loops_on_strokes: BoolProperty(
                    name="Loops on strokes",
                    description="Make the loops match the paths of the strokes",
                    default=False
                    )
    automatic_join: BoolProperty(
                    name="Automatic join",
                    description="Join automatically vertices of either surfaces generated "
                                "by crosshatching, or from the borders of closed shapes",
                    default=False
                    )
    join_stretch_factor: FloatProperty(
                    name="Stretch",
                    description="Amount of stretching or shrinking allowed for "
                                "edges when joining vertices automatically",
                    default=1,
                    min=0,
                    max=3,
                    subtype='FACTOR'
                    )
    keep_strokes: BoolProperty(
                name="Keep strokes",
                description="Keeps the sketched strokes or curves after adding the surface",
                default=False
                )
    strokes_type: StringProperty()
    initial_global_undo_state: BoolProperty()
    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        row = layout.row()
        if not self.is_fill_faces:
            row.separator()
            if not self.is_crosshatch:
                if not self.selection_U_exists:
                    col.prop(self, "edges_U")
                    row.separator()
                if not self.selection_V_exists:
                    col.prop(self, "edges_V")
                    row.separator()
                row.separator()
                if not self.selection_U_exists:
                    if not (
                          (self.selection_V_exists and not self.selection_V_is_closed) or
                          (self.selection_V2_exists and not self.selection_V2_is_closed)
                          ):
                        col.prop(self, "cyclic_cross")
                if not self.selection_V_exists:
                    if not (
                          (self.selection_U_exists and not self.selection_U_is_closed) or
                          (self.selection_U2_exists and not self.selection_U2_is_closed)
                          ):
                        col.prop(self, "cyclic_follow")
                col.prop(self, "loops_on_strokes")
            if self.automatic_join:
                row.separator()
                col.separator()
                row.separator()
                col.prop(self, "join_stretch_factor")
            col.prop(self, "keep_strokes")
    # Get an ordered list of a chain of vertices
    def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx,
                          first_vert_idx, middle_vertex_idx, closing_vert_idx):
        # Order selected vertices.
        verts_ordered = []
        if closing_vert_idx is not None:
            verts_ordered.append(ob.data.vertices[closing_vert_idx])
        verts_ordered.append(ob.data.vertices[first_vert_idx])
        prev_v = first_vert_idx
        prev_ed = None
        finish_while = False
        while True:
            edges_non_matched = 0
            for i in all_selected_edges_idx:
                if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and \
                   ob.data.edges[i].vertices[1] in all_selected_verts_idx:

                    verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
                    prev_v = ob.data.edges[i].vertices[1]
                    prev_ed = ob.data.edges[i]
                elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and \
                     ob.data.edges[i].vertices[0] in all_selected_verts_idx:

                    verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
                    prev_v = ob.data.edges[i].vertices[0]
                    prev_ed = ob.data.edges[i]
                else:
                    edges_non_matched += 1
                    if edges_non_matched == len(all_selected_edges_idx):
                        finish_while = True
            if finish_while:
                break
        if closing_vert_idx is not None:
            verts_ordered.append(ob.data.vertices[closing_vert_idx])
        if middle_vertex_idx is not None:
            verts_ordered.append(ob.data.vertices[middle_vertex_idx])
            verts_ordered.reverse()
        return tuple(verts_ordered)
    # Calculates length of a chain of points.
Eclectiel L's avatar
Eclectiel L committed
    def get_chain_length(self, object, verts_ordered):
        matrix = object.matrix_world
        edges_lengths = []
        edges_lengths_sum = 0
        for i in range(0, len(verts_ordered)):
            if i == 0:
                prev_v_co = matrix @ verts_ordered[i].co
                v_co = matrix @ verts_ordered[i].co
Eclectiel L's avatar
Eclectiel L committed
                v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
                edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
                edges_lengths.append(edge_length)
                edges_lengths_sum += edge_length
Eclectiel L's avatar
Eclectiel L committed
                prev_v_co = v_co
        return edges_lengths, edges_lengths_sum
    # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
    def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
        edges_proportions = []
        if use_boundaries:
            verts_count = 1
            for l in edges_lengths:
                edges_proportions.append(l / edges_lengths_sum)
                verts_count += 1
        else:
            verts_count = 1
            for _n in range(0, fixed_edges_num):
                edges_proportions.append(1 / fixed_edges_num)
                verts_count += 1
        return edges_proportions
    # Calculates the angle between two pairs of points in space
    def orientation_difference(self, points_A_co, points_B_co):
        # each parameter should be a list with two elements,
        # and each element should be a x,y,z coordinate
        vec_A = points_A_co[0] - points_A_co[1]
        vec_B = points_B_co[0] - points_B_co[1]
        angle = vec_A.angle(vec_B)
        if angle > 0.5 * pi:
            angle = abs(angle - pi)
    # Calculate the which vert of verts_idx list is the nearest one
    # to the point_co coordinates, and the distance
    def shortest_distance(self, object, point_co, verts_idx):
        matrix = object.matrix_world
        for i in range(0, len(verts_idx)):
            dist = (point_co - matrix @ object.data.vertices[verts_idx[i]].co).length
            if i == 0:
                prev_dist = dist
                nearest_vert_idx = verts_idx[i]
                shortest_dist = dist
            if dist < prev_dist:
                prev_dist = dist
                nearest_vert_idx = verts_idx[i]
                shortest_dist = dist
        return nearest_vert_idx, shortest_dist
    # Returns the index of the opposite vert tip in a chain, given a vert tip index
    # as parameter, and a multidimentional list with all pairs of tips
    def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
        opposite_vert_tip_idx = None
        for i in range(0, len(all_chains_tips_idx)):
            if vert_tip_idx == all_chains_tips_idx[i][0]:
                opposite_vert_tip_idx = all_chains_tips_idx[i][1]
            if vert_tip_idx == all_chains_tips_idx[i][1]:
                opposite_vert_tip_idx = all_chains_tips_idx[i][0]
        return opposite_vert_tip_idx
    # Simplifies a spline and returns the new points coordinates
    def simplify_spline(self, spline_coords, segments_num):
        simplified_spline = []
        points_between_segments = round(len(spline_coords) / segments_num)
        simplified_spline.append(spline_coords[0])
        for i in range(1, segments_num):
            simplified_spline.append(spline_coords[i * points_between_segments])
        simplified_spline.append(spline_coords[len(spline_coords) - 1])
        return simplified_spline
    # Returns a list with the coords of the points distributed over the splines
    # passed to this method according to the proportions parameter
    def distribute_pts(self, surface_splines, proportions):

        # Calculate the length of each final surface spline
        surface_splines_lengths = []
        surface_splines_parsed = []
        for sp_idx in range(0, len(surface_splines)):
            # Calculate spline length
            surface_splines_lengths.append(0)
            for i in range(0, len(surface_splines[sp_idx].bezier_points)):
                if i == 0:
                    prev_p = surface_splines[sp_idx].bezier_points[i]
                else:
                    p = surface_splines[sp_idx].bezier_points[i]
                    edge_length = (prev_p.co - p.co).length
                    surface_splines_lengths[sp_idx] += edge_length
        # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
        for sp_idx in range(0, len(surface_splines)):
            surface_splines_parsed.append([])
            surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
            prev_p_co = surface_splines[sp_idx].bezier_points[0].co
            p_idx = 0
            for prop_idx in range(len(proportions) - 1):
                target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
                partial_segment_length = 0
                finish_while = False
                while True:
                    # if not it'll pass the p_idx as an index below and crash
                    if p_idx < len(surface_splines[sp_idx].bezier_points):
                        p_co = surface_splines[sp_idx].bezier_points[p_idx].co
                        new_dist = (prev_p_co - p_co).length
                    # The new distance that could have the partial segment if
                    # it is still shorter than the target length
                    potential_segment_length = partial_segment_length + new_dist
                    # If the potential is still shorter, keep adding
                    if potential_segment_length < target_length:
                        partial_segment_length = potential_segment_length
                        p_idx += 1
                        prev_p_co = p_co
                    # If the potential is longer than the target, calculate the target
                    # (a point between the last two points), and assign
                    elif potential_segment_length > target_length:
                        remaining_dist = target_length - partial_segment_length
                        vec = p_co - prev_p_co
                        vec.normalize()
                        intermediate_co = prev_p_co + (vec * remaining_dist)
                        surface_splines_parsed[sp_idx].append(intermediate_co)
                        partial_segment_length += remaining_dist
                        prev_p_co = intermediate_co
                        finish_while = True
                    # If the potential is equal to the target, assign
                    elif potential_segment_length == target_length:
                        surface_splines_parsed[sp_idx].append(p_co)
                        prev_p_co = p_co
                        finish_while = True
                    if finish_while:
                        break
            # last point of the spline
            surface_splines_parsed[sp_idx].append(
                    surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
                    )
        return surface_splines_parsed
    # Counts the number of faces that belong to each edge
    def edge_face_count(self, ob):
        ed_keys_count_dict = {}
        for face in ob.data.polygons:
            for ed_keys in face.edge_keys:
                if ed_keys not in ed_keys_count_dict:
                    ed_keys_count_dict[ed_keys] = 1
                else:
                    ed_keys_count_dict[ed_keys] += 1
        edge_face_count = []
        for i in range(len(ob.data.edges)):
            edge_face_count.append(0)
        for i in range(len(ob.data.edges)):
            ed = ob.data.edges[i]
            v1 = ed.vertices[0]
            v2 = ed.vertices[1]
            if (v1, v2) in ed_keys_count_dict:
                edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
            elif (v2, v1) in ed_keys_count_dict:
                edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
        return edge_face_count
    # Fills with faces all the selected vertices which form empty triangles or quads
    def fill_with_faces(self, object):
        all_selected_verts_count = self.main_object_selected_verts_count
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        # Calculate average length of selected edges
        all_selected_verts = []
        original_sel_edges_count = 0
        for ed in object.data.edges:
            if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
                coords = []
                coords.append(object.data.vertices[ed.vertices[0]].co)
                coords.append(object.data.vertices[ed.vertices[1]].co)
                original_sel_edges_count += 1
                if not ed.vertices[0] in all_selected_verts:
                    all_selected_verts.append(ed.vertices[0])
                if not ed.vertices[1] in all_selected_verts:
                    all_selected_verts.append(ed.vertices[1])
        tuple(all_selected_verts)
        # Check if there is any edge selected. If not, interrupt the script
        if original_sel_edges_count == 0 and all_selected_verts_count > 0:
            return 0
        # Get all edges connected to selected verts
        all_edges_around_sel_verts = []
        edges_connected_to_sel_verts = {}
        verts_connected_to_every_vert = {}
        for ed_idx in range(len(object.data.edges)):
            ed = object.data.edges[ed_idx]
            include_edge = False
            if ed.vertices[0] in all_selected_verts:
                if not ed.vertices[0] in edges_connected_to_sel_verts:
                    edges_connected_to_sel_verts[ed.vertices[0]] = []
                edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
                include_edge = True
            if ed.vertices[1] in all_selected_verts:
                if not ed.vertices[1] in edges_connected_to_sel_verts:
                    edges_connected_to_sel_verts[ed.vertices[1]] = []
                edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
                include_edge = True
            if include_edge is True:
                all_edges_around_sel_verts.append(ed_idx)
            # Get all connected verts to each vert
            if not ed.vertices[0] in verts_connected_to_every_vert:
                verts_connected_to_every_vert[ed.vertices[0]] = []
            if not ed.vertices[1] in verts_connected_to_every_vert:
                verts_connected_to_every_vert[ed.vertices[1]] = []
            verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
            verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
        # Get all verts connected to faces
        all_verts_part_of_faces = []
        all_edges_faces_count = []
        all_edges_faces_count += self.edge_face_count(object)
        # Get only the selected edges that have faces attached.
        count_faces_of_edges_around_sel_verts = {}
        selected_verts_with_faces = []
        for ed_idx in all_edges_around_sel_verts:
            count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
            if all_edges_faces_count[ed_idx] > 0:
                ed = object.data.edges[ed_idx]
                if not ed.vertices[0] in selected_verts_with_faces:
                    selected_verts_with_faces.append(ed.vertices[0])
                if not ed.vertices[1] in selected_verts_with_faces:
                    selected_verts_with_faces.append(ed.vertices[1])
                all_verts_part_of_faces.append(ed.vertices[0])
                all_verts_part_of_faces.append(ed.vertices[1])
        tuple(selected_verts_with_faces)
        # Discard unneeded verts from calculations
        participating_verts = []
        movable_verts = []
        for v_idx in all_selected_verts:
            vert_has_edges_with_one_face = False
            # Check if the actual vert has at least one edge connected to only one face
            for ed_idx in edges_connected_to_sel_verts[v_idx]:
                if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
                    vert_has_edges_with_one_face = True
            # If the vert has two or less edges connected and the vert is not part of any face.
            # Or the vert is part of any face and at least one of
            # the connected edges has only one face attached to it.
            if (len(edges_connected_to_sel_verts[v_idx]) == 2 and
               v_idx not in all_verts_part_of_faces) or \
               len(edges_connected_to_sel_verts[v_idx]) == 1 or \
               (v_idx in all_verts_part_of_faces and
               vert_has_edges_with_one_face):

                participating_verts.append(v_idx)
                if v_idx not in all_verts_part_of_faces:
                    movable_verts.append(v_idx)
        # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
        for mv_idx in movable_verts:
            freeze_vert = False
            mv_connected_verts = verts_connected_to_every_vert[mv_idx]
            for actual_v_idx in all_selected_verts:
                count_shared_neighbors = 0
                checked_verts = []
                for mv_conn_v_idx in mv_connected_verts:
                    if mv_idx != actual_v_idx:
                        if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and \
                           mv_conn_v_idx not in checked_verts:
                            count_shared_neighbors += 1
                            checked_verts.append(mv_conn_v_idx)
                            if actual_v_idx in mv_connected_verts:
                                freeze_vert = True
                                break
                        if count_shared_neighbors == 2:
                            freeze_vert = True
                            break
                if freeze_vert:
                    break
            if freeze_vert:
                movable_verts.remove(mv_idx)
        # Calculate merge distance for participating verts
        shortest_edge_length = None
        for ed in object.data.edges:
            if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
                v1 = object.data.vertices[ed.vertices[0]]
                v2 = object.data.vertices[ed.vertices[1]]
                length = (v1.co - v2.co).length
                if shortest_edge_length is None:
                    shortest_edge_length = length
                else:
                    if length < shortest_edge_length:
                        shortest_edge_length = length
        if shortest_edge_length is not None:
            edges_merge_distance = shortest_edge_length * 0.5
        else:
            edges_merge_distance = 0
        # Get together the verts near enough. They will be merged later
        remaining_verts = []
        remaining_verts += participating_verts
        for v1_idx in participating_verts:
            if v1_idx in remaining_verts and v1_idx in movable_verts:
                verts_to_merge = []
                coords_verts_to_merge = {}
                verts_to_merge.append(v1_idx)
                v1_co = object.data.vertices[v1_idx].co
                coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
                for v2_idx in remaining_verts:
                    if v1_idx != v2_idx:
                        v2_co = object.data.vertices[v2_idx].co
                        dist = (v1_co - v2_co).length
                        if dist <= edges_merge_distance:  # Add the verts which are near enough
                            verts_to_merge.append(v2_idx)
                            coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
                for vm_idx in verts_to_merge:
                    remaining_verts.remove(vm_idx)
                if len(verts_to_merge) > 1:
                    # Calculate middle point of the verts to merge.
                    sum_x_co = 0
                    sum_y_co = 0
                    sum_z_co = 0
                    movable_verts_to_merge_count = 0
                    for i in range(len(verts_to_merge)):
                        if verts_to_merge[i] in movable_verts:
                            v_co = object.data.vertices[verts_to_merge[i]].co
                            sum_x_co += v_co[0]
                            sum_y_co += v_co[1]
                            sum_z_co += v_co[2]
                            movable_verts_to_merge_count += 1
                    middle_point_co = [
                                sum_x_co / movable_verts_to_merge_count,
                                sum_y_co / movable_verts_to_merge_count,
                                sum_z_co / movable_verts_to_merge_count
                                ]
                    # Check if any vert to be merged is not movable
                    shortest_dist = None
                    are_verts_not_movable = False
                    verts_not_movable = []
                    for v_merge_idx in verts_to_merge:
                        if v_merge_idx in participating_verts and v_merge_idx not in movable_verts:
                            are_verts_not_movable = True
                            verts_not_movable.append(v_merge_idx)
                    if are_verts_not_movable:
                        # Get the vert connected to faces, that is nearest to
                        # the middle point of the movable verts
                        shortest_dist = None
                        for vcf_idx in verts_not_movable:
                                dist = abs((object.data.vertices[vcf_idx].co -
                                            Vector(middle_point_co)).length)
                                if shortest_dist is None:
                                    shortest_dist = dist
                                    nearest_vert_idx = vcf_idx
                                else:
                                    if dist < shortest_dist:
                                        shortest_dist = dist
                                        nearest_vert_idx = vcf_idx
                        coords = object.data.vertices[nearest_vert_idx].co
                        target_point_co = [coords[0], coords[1], coords[2]]
                        target_point_co = middle_point_co
                    # Move verts to merge to the middle position
                    for v_merge_idx in verts_to_merge:
                        if v_merge_idx in movable_verts:  # Only move the verts that are not part of faces
                            object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
                            object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
                            object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
        # Perform "Remove Doubles" to weld all the disconnected verts
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
        bpy.ops.mesh.remove_doubles(threshold=0.0001)
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        # Get all the definitive selected edges, after weldding
        selected_edges = []
        edges_per_vert = {}  # Number of faces of each selected edge
        for ed in object.data.edges:
            if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
                selected_edges.append(ed.index)
                # Save all the edges that belong to each vertex.
                if not ed.vertices[0] in edges_per_vert:
                    edges_per_vert[ed.vertices[0]] = []
                if not ed.vertices[1] in edges_per_vert:
                    edges_per_vert[ed.vertices[1]] = []
                edges_per_vert[ed.vertices[0]].append(ed.index)
                edges_per_vert[ed.vertices[1]].append(ed.index)
        # Check if all the edges connected to each vert have two faces attached to them.
        # To discard them later and make calculations faster
        a = []
        a += self.edge_face_count(object)
        tuple(a)
        verts_surrounded_by_faces = {}
        for v_idx in edges_per_vert:
            edges_with_two_faces_count = 0
            for ed_idx in edges_per_vert[v_idx]:
                if a[ed_idx] == 2:
                    edges_with_two_faces_count += 1
            if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
                verts_surrounded_by_faces[v_idx] = True
            else:
                verts_surrounded_by_faces[v_idx] = False
        # Get all the selected vertices
        selected_verts_idx = []
        for v in object.data.vertices:
            if v.select:
                selected_verts_idx.append(v.index)
        # Get all the faces of the object
        all_object_faces_verts_idx = []
        for face in object.data.polygons:
            face_verts = []
            face_verts.append(face.vertices[0])
            face_verts.append(face.vertices[1])
            face_verts.append(face.vertices[2])
            if len(face.vertices) == 4:
                face_verts.append(face.vertices[3])
            all_object_faces_verts_idx.append(face_verts)
        # Deselect all vertices
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
        bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        # Make a dictionary with the verts related to each vert
        related_key_verts = {}
        for ed_idx in selected_edges:
            ed = object.data.edges[ed_idx]
            if not verts_surrounded_by_faces[ed.vertices[0]]:
                if not ed.vertices[0] in related_key_verts:
                    related_key_verts[ed.vertices[0]] = []
                if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
                    related_key_verts[ed.vertices[0]].append(ed.vertices[1])
            if not verts_surrounded_by_faces[ed.vertices[1]]:
                if not ed.vertices[1] in related_key_verts:
                    related_key_verts[ed.vertices[1]] = []
                if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
                    related_key_verts[ed.vertices[1]].append(ed.vertices[0])
        # Get groups of verts forming each face
        for v1 in related_key_verts:      # verts-1 ....
            for v2 in related_key_verts:  # verts-2
                if v1 != v2:
                    related_verts_in_common = []
                    v2_in_rel_v1 = False
                    v1_in_rel_v2 = False
                    for rel_v1 in related_key_verts[v1]:
                        # Check if related verts of verts-1 are related verts of verts-2
                        if rel_v1 in related_key_verts[v2]:
                            related_verts_in_common.append(rel_v1)
                    if v2 in related_key_verts[v1]:
                        v2_in_rel_v1 = True
                    if v1 in related_key_verts[v2]:
                        v1_in_rel_v2 = True
                    repeated_face = False
                    # If two verts have two related verts in common, they form a quad
                    if len(related_verts_in_common) == 2:
                        # Check if the face is already saved
                        all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
                        for f_verts in all_faces_to_check_idx:
                            repeated_verts = 0
                            if len(f_verts) == 4:
                                if v1 in f_verts:
                                    repeated_verts += 1
                                if v2 in f_verts:
                                    repeated_verts += 1
                                if related_verts_in_common[0] in f_verts:
                                    repeated_verts += 1
                                if related_verts_in_common[1] in f_verts:
                                    repeated_verts += 1
                                if repeated_verts == len(f_verts):
                                    repeated_face = True
                                    break
                        if not repeated_face:
                            faces_verts_idx.append(
                                    [v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
                                    )
                    # If Two verts have one related vert in common and
                    # they are related to each other, they form a triangle
                    elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
                        # Check if the face is already saved.
                        all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
                        for f_verts in all_faces_to_check_idx:
                            repeated_verts = 0
                            if len(f_verts) == 3:
                                if v1 in f_verts:
                                    repeated_verts += 1
                                if v2 in f_verts:
                                    repeated_verts += 1
                                if related_verts_in_common[0] in f_verts:
                                    repeated_verts += 1
                                if repeated_verts == len(f_verts):
                                    repeated_face = True
                                    break
                        if not repeated_face:
                            faces_verts_idx.append([v1, related_verts_in_common[0], v2])
        # Keep only the faces that don't overlap by ignoring quads
        # that overlap with two adjacent triangles
        faces_to_not_include_idx = []  # Indices of faces_verts_idx to eliminate
        all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
        for i in range(len(faces_verts_idx)):
            for t in range(len(all_faces_to_check_idx)):
                if i != t:
                    verts_in_common = 0
                    if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
                        for v_idx in all_faces_to_check_idx[t]:
                            if v_idx in faces_verts_idx[i]:
                                verts_in_common += 1
                        # If it doesn't have all it's vertices repeated in the other face
                        if verts_in_common == 3:
                            if i not in faces_to_not_include_idx:
                                faces_to_not_include_idx.append(i)
        # Build faces discarding the ones in faces_to_not_include
        me = object.data
        bm = bmesh.new()
        bm.from_mesh(me)
        num_faces_created = 0
        for i in range(len(faces_verts_idx)):
            if i not in faces_to_not_include_idx:
                bm.faces.new([bm.verts[v] for v in faces_verts_idx[i]])
                num_faces_created += 1
        bm.to_mesh(me)
        bm.free()
        for v_idx in selected_verts_idx:
            self.main_object.data.vertices[v_idx].select = True
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
        bpy.ops.mesh.normals_make_consistent(inside=False)
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        return num_faces_created
    # Crosshatch skinning
    def crosshatch_surface_invoke(self, ob_original_splines):
        self.is_crosshatch = False
        self.crosshatch_merge_distance = 0
        objects_to_delete = []  # duplicated strokes to be deleted.
        # If the main object uses modifiers deactivate them temporarily until the surface is joined
        # (without this the surface verts merging with the main object doesn't work well)
        self.modifiers_prev_viewport_state = []
        if len(self.main_object.modifiers) > 0:
            for m_idx in range(len(self.main_object.modifiers)):
                self.modifiers_prev_viewport_state.append(
                                    self.main_object.modifiers[m_idx].show_viewport
                                    )
                self.main_object.modifiers[m_idx].show_viewport = False
        bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
        ob_original_splines.select_set(True)
        bpy.context.view_layer.objects.active = ob_original_splines
        if len(ob_original_splines.data.splines) >= 2:
            bpy.ops.object.duplicate('INVOKE_REGION_WIN')
            ob_splines = bpy.context.object
            ob_splines.name = "SURFSKIO_NE_STR"
            # Get estimative merge distance (sum up the distances from the first point to
            # all other points, then average them and then divide them)
            first_point_dist_sum = 0
            first_dist = 0
            second_dist = 0
            coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
            for i in range(len(ob_splines.data.splines)):
                sp = ob_splines.data.splines[i]
                if coords_first_pt != sp.bezier_points[0].co:
                    first_dist = (coords_first_pt - sp.bezier_points[0].co).length
                if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
                    second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
                first_point_dist_sum += first_dist + second_dist
                if i == 0:
                    if first_dist != 0:
                        shortest_dist = first_dist
                    elif second_dist != 0:
                        shortest_dist = second_dist
                if shortest_dist > first_dist and first_dist != 0:
                    shortest_dist = first_dist
                if shortest_dist > second_dist and second_dist != 0:
                    shortest_dist = second_dist
            self.crosshatch_merge_distance = shortest_dist / 20
            # Recalculation of merge distance
            bpy.ops.object.duplicate('INVOKE_REGION_WIN')
            ob_calc_merge_dist = bpy.context.object
            ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
            objects_to_delete.append(ob_calc_merge_dist)
            # Smooth out strokes a little to improve crosshatch detection
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
            for i in range(4):
                bpy.ops.curve.smooth('INVOKE_REGION_WIN')
            bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            # Convert curves into mesh
            ob_calc_merge_dist.data.resolution_u = 12
            bpy.ops.object.convert(target='MESH', keep_original=False)
            # Find "intersection-nodes"
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
            bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
                                        threshold=self.crosshatch_merge_distance)
            bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            # Remove verts with less than three edges
            verts_edges_count = {}
            for ed in ob_calc_merge_dist.data.edges:
                v = ed.vertices
                if v[0] not in verts_edges_count:
                    verts_edges_count[v[0]] = 0
                if v[1] not in verts_edges_count:
                    verts_edges_count[v[1]] = 0
                verts_edges_count[v[0]] += 1
                verts_edges_count[v[1]] += 1
            nodes_verts_coords = []
            for v_idx in verts_edges_count:
                v = ob_calc_merge_dist.data.vertices[v_idx]
                if verts_edges_count[v_idx] < 3:
                    v.select = True
            # Remove them
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
            bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
            # Remove doubles to discard very near verts from calculations of distance
            bpy.ops.mesh.remove_doubles(
                        'INVOKE_REGION_WIN',
                        threshold=self.crosshatch_merge_distance * 4.0
                        )
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            # Get all coords of the resulting nodes
            nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for
                                   v in ob_calc_merge_dist.data.vertices]
            # Check if the strokes are a crosshatch
            if len(nodes_verts_coords) >= 3:
                self.is_crosshatch = True
                shortest_dist = None
                for co_1 in nodes_verts_coords:
                    for co_2 in nodes_verts_coords:
                        if co_1 != co_2:
                            dist = (Vector(co_1) - Vector(co_2)).length
                            if shortest_dist is not None:
                                if dist < shortest_dist:
                                    shortest_dist = dist
                            else:
                                shortest_dist = dist
                self.crosshatch_merge_distance = shortest_dist / 3
            bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
            ob_splines.select_set(True)
            bpy.context.view_layer.objects.active = ob_splines
            # Deselect all points
            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')
            # Smooth splines in a localized way, to eliminate "saw-teeth"
            # like shapes when there are many points
            for sp in ob_splines.data.splines:
                angle_sum = 0
                angle_limit = 2  # Degrees
                for t in range(len(sp.bezier_points)):
                    # Because on each iteration it checks the "next two points"
                    # of the actual. This way it doesn't go out of range
                    if t <= len(sp.bezier_points) - 3:
                        p1 = sp.bezier_points[t]
                        p2 = sp.bezier_points[t + 1]
                        p3 = sp.bezier_points[t + 2]
                        vec_1 = p1.co - p2.co
                        vec_2 = p2.co - p3.co
                        if p2.co != p1.co and p2.co != p3.co:
                            angle = vec_1.angle(vec_2)
                            angle_sum += degrees(angle)
                            if angle_sum >= angle_limit:  # If sum of angles is grater than the limit
                                if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
                                    p1.select_control_point = True
                                    p1.select_left_handle = True
                                    p1.select_right_handle = True

                                    p2.select_control_point = True
                                    p2.select_left_handle = True
                                    p2.select_right_handle = True
                                if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
                                    p3.select_control_point = True
                                    p3.select_left_handle = True
                                    p3.select_right_handle = True
                                angle_sum = 0
                sp.bezier_points[0].select_control_point = False
                sp.bezier_points[0].select_left_handle = False
                sp.bezier_points[0].select_right_handle = False
                sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
                sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
                sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
            # Smooth out strokes a little to improve crosshatch detection
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            for i in range(15):
                bpy.ops.curve.smooth('INVOKE_REGION_WIN')
            bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            # Simplify the splines
            for sp in ob_splines.data.splines:
                angle_sum = 0
                sp.bezier_points[0].select_control_point = True
                sp.bezier_points[0].select_left_handle = True
                sp.bezier_points[0].select_right_handle = True
                sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
                sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
                sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
                angle_limit = 15  # Degrees
                for t in range(len(sp.bezier_points)):
                    # Because on each iteration it checks the "next two points"
                    # of the actual. This way it doesn't go out of range
                    if t <= len(sp.bezier_points) - 3:
                        p1 = sp.bezier_points[t]
                        p2 = sp.bezier_points[t + 1]
                        p3 = sp.bezier_points[t + 2]
                        vec_1 = p1.co - p2.co
                        vec_2 = p2.co - p3.co
                        if p2.co != p1.co and p2.co != p3.co:
                            angle = vec_1.angle(vec_2)
                            angle_sum += degrees(angle)
                            # If sum of angles is grater than the limit
                            if angle_sum >= angle_limit:
                                p1.select_control_point = True
                                p1.select_left_handle = True
                                p1.select_right_handle = True
                                p2.select_control_point = True
                                p2.select_left_handle = True
                                p2.select_right_handle = True
                                p3.select_control_point = True
                                p3.select_left_handle = True
                                p3.select_right_handle = True
                                angle_sum = 0
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            bpy.ops.curve.select_all(action='INVERT')
            bpy.ops.curve.delete(type='VERT')
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            objects_to_delete.append(ob_splines)
            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')
            # Check if the strokes are a crosshatch
            if self.is_crosshatch:
                all_points_coords = []
                for i in range(len(ob_splines.data.splines)):
                    all_points_coords.append([])
                    all_points_coords[i] = [Vector((x, y, z)) for
                                            x, y, z in [bp.co for
                                            bp in ob_splines.data.splines[i].bezier_points]]
                all_intersections = []
                checked_splines = []
                for i in range(len(all_points_coords)):
                    for t in range(len(all_points_coords[i]) - 1):
                        bp1_co = all_points_coords[i][t]
                        bp2_co = all_points_coords[i][t + 1]
                        for i2 in range(len(all_points_coords)):
                            if i != i2 and i2 not in checked_splines:
                                for t2 in range(len(all_points_coords[i2]) - 1):
                                    bp3_co = all_points_coords[i2][t2]
                                    bp4_co = all_points_coords[i2][t2 + 1]
                                    intersec_coords = intersect_line_line(
                                                            bp1_co, bp2_co, bp3_co, bp4_co
                                                            )
                                    if intersec_coords is not None:
                                        dist = (intersec_coords[0] - intersec_coords[1]).length
                                        if dist <= self.crosshatch_merge_distance * 1.5:
                                            _temp_co, percent1 = intersect_point_line(
                                                                    intersec_coords[0], bp1_co, bp2_co
                                                                    )
                                            if (percent1 >= -0.02 and percent1 <= 1.02):
                                                _temp_co, percent2 = intersect_point_line(
                                                                        intersec_coords[1], bp3_co, bp4_co
                                                                        )
                                                if (percent2 >= -0.02 and percent2 <= 1.02):
                                                    # Format: spline index, first point index from
                                                    # corresponding segment, percentage from first point of
                                                    # actual segment, coords of intersection point
                                                    all_intersections.append(
                                                            (i, t, percent1,
                                                            ob_splines.matrix_world @ intersec_coords[0])
                                                            )
                                                    all_intersections.append(
                                                            (i2, t2, percent2,
                                                            ob_splines.matrix_world @ intersec_coords[1])
                # Sort list by spline, then by corresponding first point index of segment,
                # and then by percentage from first point of segment: elements 0 and 1 respectively
                all_intersections.sort(key=operator.itemgetter(0, 1, 2))
                self.crosshatch_strokes_coords = {}
                for i in range(len(all_intersections)):
                    if not all_intersections[i][0] in self.crosshatch_strokes_coords:
                        self.crosshatch_strokes_coords[all_intersections[i][0]] = []
                    self.crosshatch_strokes_coords[all_intersections[i][0]].append(
                                                                        all_intersections[i][3]
                                                                        )  # Save intersection coords
            else:
                self.is_crosshatch = False
        # Delete all duplicates
        bpy.ops.object.delete({"selected_objects": objects_to_delete})
        # If the main object has modifiers, turn their "viewport view status" to
        # what it was before the forced deactivation above
        if len(self.main_object.modifiers) > 0:
            for m_idx in range(len(self.main_object.modifiers)):
                self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
    # Part of the Crosshatch process that is repeated when the operator is tweaked
    def crosshatch_surface_execute(self, context):
        # If the main object uses modifiers deactivate them temporarily until the surface is joined
        # (without this the surface verts merging with the main object doesn't work well)
        self.modifiers_prev_viewport_state = []
        if len(self.main_object.modifiers) > 0:
            for m_idx in range(len(self.main_object.modifiers)):
                self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
                self.main_object.modifiers[m_idx].show_viewport = False
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        me_name = "SURFSKIO_STK_TMP"
        me = bpy.data.meshes.new(me_name)
        all_verts_coords = []
        all_edges = []
        for st_idx in self.crosshatch_strokes_coords:
            for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
                coords = self.crosshatch_strokes_coords[st_idx][co_idx]
                all_verts_coords.append(coords)
                if co_idx > 0:
                    all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
        me.from_pydata(all_verts_coords, all_edges, [])
        ob = object_utils.object_data_add(context, me)
        ob.location = (0.0, 0.0, 0.0)
        ob.rotation_euler = (0.0, 0.0, 0.0)
        ob.scale = (1.0, 1.0, 1.0)
        bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
        ob.select_set(True)
        bpy.context.view_layer.objects.active = ob
        # Get together each vert and its nearest, to the middle position
        verts = ob.data.vertices
        checked_verts = []
        for i in range(len(verts)):
            shortest_dist = None
            if i not in checked_verts:
                for t in range(len(verts)):
                    if i != t and t not in checked_verts:
                        dist = (verts[i].co - verts[t].co).length
                        if shortest_dist is not None:
                            if dist < shortest_dist:
                                shortest_dist = dist
                                nearest_vert = t
                        else:
                            shortest_dist = dist
                            nearest_vert = t
                middle_location = (verts[i].co + verts[nearest_vert].co) / 2
                verts[i].co = middle_location
                verts[nearest_vert].co = middle_location
                checked_verts.append(i)
                checked_verts.append(nearest_vert)
        # Calculate average length between all the generated edges
        ob = bpy.context.object
        lengths_sum = 0
        for ed in ob.data.edges:
            v1 = ob.data.vertices[ed.vertices[0]]
            v2 = ob.data.vertices[ed.vertices[1]]
            lengths_sum += (v1.co - v2.co).length
        edges_count = len(ob.data.edges)
        # possible division by zero here
        average_edge_length = lengths_sum / edges_count if edges_count != 0 else 0.0001
Eclectiel L's avatar
Eclectiel L committed
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
        bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
                                    threshold=average_edge_length / 15.0)
Eclectiel L's avatar
Eclectiel L committed
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        final_points_ob = bpy.context.view_layer.objects.active
        # Make a dictionary with the verts related to each vert
        related_key_verts = {}
        for ed in final_points_ob.data.edges:
            if not ed.vertices[0] in related_key_verts:
                related_key_verts[ed.vertices[0]] = []
            if not ed.vertices[1] in related_key_verts:
                related_key_verts[ed.vertices[1]] = []
            if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
                related_key_verts[ed.vertices[0]].append(ed.vertices[1])
            if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
                related_key_verts[ed.vertices[1]].append(ed.vertices[0])
        # Get groups of verts forming each face
        for v1 in related_key_verts:      # verts-1 ....
            for v2 in related_key_verts:  # verts-2
                if v1 != v2:
                    related_verts_in_common = []
                    v2_in_rel_v1 = False
                    v1_in_rel_v2 = False
                    for rel_v1 in related_key_verts[v1]:
                        # Check if related verts of verts-1 are related verts of verts-2
                        if rel_v1 in related_key_verts[v2]:
                            related_verts_in_common.append(rel_v1)
                    if v2 in related_key_verts[v1]:
                        v2_in_rel_v1 = True
                    if v1 in related_key_verts[v2]:
                        v1_in_rel_v2 = True
                    repeated_face = False
                    # If two verts have two related verts in common, they form a quad
                    if len(related_verts_in_common) == 2:
                        # Check if the face is already saved
                        for f_verts in faces_verts_idx:
                            repeated_verts = 0
                            if len(f_verts) == 4:
                                if v1 in f_verts:
                                    repeated_verts += 1
                                if v2 in f_verts:
                                    repeated_verts += 1
                                if related_verts_in_common[0] in f_verts:
                                    repeated_verts += 1
                                if related_verts_in_common[1] in f_verts:
                                    repeated_verts += 1
                                if repeated_verts == len(f_verts):
                                    repeated_face = True
                                    break
                        if not repeated_face:
                            faces_verts_idx.append([v1, related_verts_in_common[0],
                                                    v2, related_verts_in_common[1]])
                    # If Two verts have one related vert in common and they are
                    # related to each other, they form a triangle
                    elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
                        # Check if the face is already saved.
                        for f_verts in faces_verts_idx:
                            repeated_verts = 0
                            if len(f_verts) == 3:
                                if v1 in f_verts:
                                    repeated_verts += 1
                                if v2 in f_verts:
                                    repeated_verts += 1
                                if related_verts_in_common[0] in f_verts:
                                    repeated_verts += 1
                                if repeated_verts == len(f_verts):
                                    repeated_face = True
                                    break
                        if not repeated_face:
                            faces_verts_idx.append([v1, related_verts_in_common[0], v2])
        # Keep only the faces that don't overlap by ignoring
        # quads that overlap with two adjacent triangles
        faces_to_not_include_idx = []  # Indices of faces_verts_idx to eliminate
        for i in range(len(faces_verts_idx)):
            for t in range(len(faces_verts_idx)):
                if i != t:
                    verts_in_common = 0
                    if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
                        for v_idx in faces_verts_idx[t]:
                            if v_idx in faces_verts_idx[i]:
                                verts_in_common += 1
                        # If it doesn't have all it's vertices repeated in the other face
                        if verts_in_common == 3:
                            if i not in faces_to_not_include_idx:
                                faces_to_not_include_idx.append(i)
        # Build surface
        all_surface_verts_co = []
        for i in range(len(final_points_ob.data.vertices)):
            coords = final_points_ob.data.vertices[i].co
            all_surface_verts_co.append([coords[0], coords[1], coords[2]])
        # Verts of each face.
        all_surface_faces = []
        for i in range(len(faces_verts_idx)):
            if i not in faces_to_not_include_idx:
                face = []
                for v_idx in faces_verts_idx[i]:
                    face.append(v_idx)
                all_surface_faces.append(face)
        # 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)
        ob_surface = object_utils.object_data_add(context, me_surf)
        ob_surface.location = (0.0, 0.0, 0.0)
        ob_surface.rotation_euler = (0.0, 0.0, 0.0)
        ob_surface.scale = (1.0, 1.0, 1.0)
        # Delete final points temporal object
        bpy.ops.object.delete({"selected_objects": [final_points_ob]})
        # Delete isolated verts if there are any
        bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
        ob_surface.select_set(True)
        bpy.context.view_layer.objects.active = ob_surface
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        bpy.ops.mesh.select_all(action='DESELECT')
        bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
        bpy.ops.mesh.delete()
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        # Join crosshatch results with original mesh
        # Calculate a distance to merge the verts of the crosshatch surface to the main object
        edges_length_sum = 0
        for ed in ob_surface.data.edges:
            edges_length_sum += (
                                ob_surface.data.vertices[ed.vertices[0]].co -
                                ob_surface.data.vertices[ed.vertices[1]].co
                                ).length
        # Make dictionary with all the verts connected to each vert, on the new surface object.
        surface_connected_verts = {}
        for ed in ob_surface.data.edges:
            if not ed.vertices[0] in surface_connected_verts:
                surface_connected_verts[ed.vertices[0]] = []
            surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
            if ed.vertices[1] not in surface_connected_verts:
                surface_connected_verts[ed.vertices[1]] = []
            surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
        # Duplicate the new surface object, and use shrinkwrap to
        # calculate later the nearest verts to the main object
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        bpy.ops.object.duplicate('INVOKE_REGION_WIN')
        final_ob_duplicate = bpy.context.view_layer.objects.active
        shrinkwrap_modifier = context.object.modifiers.new("", 'SHRINKWRAP')
        shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
        shrinkwrap_modifier.target = self.main_object
        bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier=shrinkwrap_modifier.name)
        # Make list with verts of original mesh as index and coords as value
        main_object_verts_coords = []
        for v in self.main_object.data.vertices:
            coords = self.main_object.matrix_world @ v.co
            # To avoid problems when taking "-0.00" as a different value as "0.00"
            for c in range(len(coords)):
                if "%.3f" % coords[c] == "-0.00":
                    coords[c] = 0
            main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
        tuple(main_object_verts_coords)
        # Determine which verts will be merged, snap them to the nearest verts
        # on the original verts, and get them selected
        crosshatch_verts_to_merge = []
        if self.automatic_join:
            for i in range(len(ob_surface.data.vertices)-1):
                # Calculate the distance from each of the connected verts to the actual vert,
                # and compare it with the distance they would have if joined.
                # If they don't change much, that vert can be joined
                merge_actual_vert = True
                try:
                    if len(surface_connected_verts[i]) < 4:
                        for c_v_idx in surface_connected_verts[i]:
                            points_original = []
                            points_original.append(ob_surface.data.vertices[c_v_idx].co)
                            points_original.append(ob_surface.data.vertices[i].co)
                            points_target = []
                            points_target.append(ob_surface.data.vertices[c_v_idx].co)
                            points_target.append(final_ob_duplicate.data.vertices[i].co)
                            vec_A = points_original[0] - points_original[1]
                            vec_B = points_target[0] - points_target[1]
                            dist_A = (points_original[0] - points_original[1]).length
                            dist_B = (points_target[0] - points_target[1]).length
                            if not (
                               points_original[0] == points_original[1] or
                               points_target[0] == points_target[1]
                               ):  # If any vector's length is zero
                            # Set a range of acceptable variation in the connected edges
                            if dist_B > dist_A * 1.7 * self.join_stretch_factor or \
                               dist_B < dist_A / 2 / self.join_stretch_factor or \
                               angle >= 0.15 * self.join_stretch_factor:
                                merge_actual_vert = False
                                break
                    else:
                        merge_actual_vert = False
                except:
                    self.report({'WARNING'},
                        "Crosshatch set incorrectly")
                if merge_actual_vert:
                    coords = final_ob_duplicate.data.vertices[i].co
                    # To avoid problems when taking "-0.000" as a different value as "0.00"
                    for c in range(len(coords)):
                        if "%.3f" % coords[c] == "-0.00":
                            coords[c] = 0
                    comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
                    if comparison_coords in main_object_verts_coords:
                        # Get the index of the vert with those coords in the main object
                        main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords)

                        if self.main_object.data.vertices[main_object_related_vert_idx].select is True or \
                           self.main_object_selected_verts_count == 0:
                            ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
                            ob_surface.data.vertices[i].select = True
                            crosshatch_verts_to_merge.append(i)
                            # Make sure the vert in the main object is selected,
                            # in case it wasn't selected and the "join crosshatch" option is active
                            self.main_object.data.vertices[main_object_related_vert_idx].select = True
        # Delete duplicated object
        bpy.ops.object.delete({"selected_objects": [final_ob_duplicate]})
        # Join crosshatched surface and main object
        bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
        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')
        # Perform Remove doubles to merge verts
        if not (self.automatic_join is False and self.main_object_selected_verts_count == 0):
            bpy.ops.mesh.remove_doubles(threshold=0.0001)
        bpy.ops.mesh.select_all(action='DESELECT')
        # If the main object has modifiers, turn their "viewport view status"
        # to what it was before the forced deactivation above
        if len(self.main_object.modifiers) > 0:
            for m_idx in range(len(self.main_object.modifiers)):
                self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
        return {'FINISHED'}
    def rectangular_surface(self, context):
        # Selected edges
        all_selected_edges_idx = []
        all_selected_verts = []
        all_verts_idx = []
        for ed in self.main_object.data.edges:
            if ed.select:
                all_selected_edges_idx.append(ed.index)
                # Selected vertices
                if not ed.vertices[0] in all_selected_verts:
                    all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
                if not ed.vertices[1] in all_selected_verts:
                    all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
                # All verts (both from each edge) to determine later
                # which are at the tips (those not repeated twice)
                all_verts_idx.append(ed.vertices[0])
                all_verts_idx.append(ed.vertices[1])
        # Identify the tips and "middle-vertex" that separates U from V, if there is one
        all_chains_tips_idx = []
        for v_idx in all_verts_idx:
            if all_verts_idx.count(v_idx) < 2:
                all_chains_tips_idx.append(v_idx)
        edges_connected_to_tips = []
        for ed in self.main_object.data.edges:
            if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and \
               not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
                edges_connected_to_tips.append(ed)
        # Check closed selections
        # List with groups of three verts, where the first element of the pair is
        # the unselected vert of a closed selection and the other two elements are the
        # selected neighbor verts (it will be useful to determine which selection chain
        # the unselected vert belongs to, and determine the "middle-vertex")
        single_unselected_verts_and_neighbors = []

        # To identify a "closed" selection (a selection that is a closed chain except
        # for one vertex) find the vertex in common that have the edges connected to tips.
        # If there is a vertex in common, that one is the unselected vert that closes
        # the selection or is a "middle-vertex"
        single_unselected_verts = []
        for ed in edges_connected_to_tips:
            for ed_b in edges_connected_to_tips:
                if ed != ed_b:
                    if ed.vertices[0] == ed_b.vertices[0] and \
                       not self.main_object.data.vertices[ed.vertices[0]].select and \
                       ed.vertices[0] not in single_unselected_verts:

                        # The second element is one of the tips of the selected
                        # vertices of the closed selection
                        single_unselected_verts_and_neighbors.append(
                                        [ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]
                                        )
                        single_unselected_verts.append(ed.vertices[0])
                        break
                    elif ed.vertices[0] == ed_b.vertices[1] and \
                       not self.main_object.data.vertices[ed.vertices[0]].select and \
                       ed.vertices[0] not in single_unselected_verts:

                        single_unselected_verts_and_neighbors.append(
                                        [ed.vertices[0], ed.vertices[1], ed_b.vertices[0]]
                                        )
                        single_unselected_verts.append(ed.vertices[0])
                        break
                    elif ed.vertices[1] == ed_b.vertices[0] and \
                       not self.main_object.data.vertices[ed.vertices[1]].select and  \
                       ed.vertices[1] not in single_unselected_verts:

                        single_unselected_verts_and_neighbors.append(
                                              [ed.vertices[1], ed.vertices[0], ed_b.vertices[1]]
                                              )
                        single_unselected_verts.append(ed.vertices[1])
                        break
                    elif ed.vertices[1] == ed_b.vertices[1] and \
                       not self.main_object.data.vertices[ed.vertices[1]].select and \
                       ed.vertices[1] not in single_unselected_verts:

                        single_unselected_verts_and_neighbors.append(
                                              [ed.vertices[1], ed.vertices[0], ed_b.vertices[0]]
                                              )
                        single_unselected_verts.append(ed.vertices[1])
                        break
        middle_vertex_idx = None
        tips_to_discard_idx = []

        # Check if there is a "middle-vertex", and get its index
        for i in range(0, len(single_unselected_verts_and_neighbors)):
            actual_chain_verts = self.get_ordered_verts(
                                        self.main_object, all_selected_edges_idx,
                                        all_verts_idx, single_unselected_verts_and_neighbors[i][1],
                                        None, None
                                        )

            if single_unselected_verts_and_neighbors[i][2] != \
               actual_chain_verts[len(actual_chain_verts) - 1].index:
                middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
                tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
                tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
        # List with pairs of verts that belong to the tips of each selection chain (row)
        verts_tips_same_chain_idx = []
        if len(all_chains_tips_idx) >= 2:
            checked_v = []
            for i in range(0, len(all_chains_tips_idx)):
                if all_chains_tips_idx[i] not in checked_v:
                    v_chain = self.get_ordered_verts(
                                    self.main_object, all_selected_edges_idx,
                                    all_verts_idx, all_chains_tips_idx[i],
                                    middle_vertex_idx, None
                                    )
                    verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
                    checked_v.append(v_chain[0].index)
                    checked_v.append(v_chain[len(v_chain) - 1].index)
        # Selection tips (vertices).
        verts_tips_parsed_idx = []
        if len(all_chains_tips_idx) >= 2:
            for spec_v_idx in all_chains_tips_idx:
                if (spec_v_idx not in tips_to_discard_idx):
                    verts_tips_parsed_idx.append(spec_v_idx)
        # Identify the type of selection made by the user
        if middle_vertex_idx is not None:
            # If there are 4 tips (two selection chains), and
            # there is only one single unselected vert (the middle vert)
            if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1:
                selection_type = "TWO_CONNECTED"
            else:
                # The type of the selection was not identified, the script stops.
                self.report({'WARNING'}, "The selection isn't valid.")
                self.stopping_errors = True
                return{'CANCELLED'}
        else:
            if len(all_chains_tips_idx) == 2:    # If there are 2 tips
                selection_type = "SINGLE"
            elif len(all_chains_tips_idx) == 4:  # If there are 4 tips
                selection_type = "TWO_NOT_CONNECTED"
            elif len(all_chains_tips_idx) == 0:
                if len(self.main_splines.data.splines) > 1:
                    selection_type = "NO_SELECTION"
                else:
                    # If the selection was not identified and there is only one stroke,
                    # there's no possibility to build a surface, so the script is interrupted
                    self.report({'WARNING'}, "The selection isn't valid.")
                    self.stopping_errors = True
                    return{'CANCELLED'}
            else:
                # The type of the selection was not identified, the script stops
                self.report({'WARNING'}, "The selection isn't valid.")
                self.stopping_errors = True
                return{'CANCELLED'}
        # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
        if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
            self.report({'WARNING'},
                        "At least two strokes are needed when there are two not connected selections")
            self.stopping_errors = True
            return{'CANCELLED'}
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        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
        # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        bpy.ops.curve.smooth('INVOKE_REGION_WIN')
        bpy.ops.curve.smooth('INVOKE_REGION_WIN')
        bpy.ops.curve.smooth('INVOKE_REGION_WIN')
        bpy.ops.curve.smooth('INVOKE_REGION_WIN')
        bpy.ops.curve.smooth('INVOKE_REGION_WIN')
        bpy.ops.curve.smooth('INVOKE_REGION_WIN')
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        self.selection_U_exists = False
        self.selection_U2_exists = False
        self.selection_V_exists = False
        self.selection_V2_exists = False
        self.selection_U_is_closed = False
        self.selection_U2_is_closed = False
        self.selection_V_is_closed = False
        self.selection_V2_is_closed = False
        # Define what vertices are at the tips of each selection and are not the middle-vertex
        if selection_type == "TWO_CONNECTED":
            self.selection_U_exists = True
            self.selection_V_exists = True
            closing_vert_U_idx = None
            closing_vert_V_idx = None
            closing_vert_U2_idx = None
            closing_vert_V2_idx = None
            # Determine which selection is Selection-U and which is Selection-V
            points_A = []
            points_B = []
            points_first_stroke_tips = []
            points_A.append(
                self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[0]].co
                )
            points_A.append(
                self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
                )
            points_B.append(
                self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[1]].co
                )
            points_B.append(
                self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
                )
            points_first_stroke_tips.append(
                self.main_splines.data.splines[0].bezier_points[0].co
                )
            points_first_stroke_tips.append(
                self.main_splines.data.splines[0].bezier_points[
                                                    len(self.main_splines.data.splines[0].bezier_points) - 1
                                                    ].co
                )
            angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
            angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
            if angle_A < angle_B:
                first_vert_U_idx = verts_tips_parsed_idx[0]
                first_vert_V_idx = verts_tips_parsed_idx[1]
            else:
                first_vert_U_idx = verts_tips_parsed_idx[1]
                first_vert_V_idx = verts_tips_parsed_idx[0]
        elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
            first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
            last_sketched_point_first_stroke_co = \
                    self.main_splines.data.splines[0].bezier_points[
                                                    len(self.main_splines.data.splines[0].bezier_points) - 1
                                                    ].co
            first_sketched_point_last_stroke_co = \
                    self.main_splines.data.splines[
                                            len(self.main_splines.data.splines) - 1
                                            ].bezier_points[0].co
            if len(self.main_splines.data.splines) > 1:
                first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
                last_sketched_point_second_stroke_co = \
                    self.main_splines.data.splines[1].bezier_points[
                                            len(self.main_splines.data.splines[1].bezier_points) - 1
                                            ].co
            single_unselected_neighbors = []  # Only the neighbors of the single unselected verts
            for verts_neig_idx in single_unselected_verts_and_neighbors:
                single_unselected_neighbors.append(verts_neig_idx[1])
                single_unselected_neighbors.append(verts_neig_idx[2])
            all_chains_tips_and_middle_vert = []
            for v_idx in all_chains_tips_idx:
                if v_idx not in single_unselected_neighbors:
                    all_chains_tips_and_middle_vert.append(v_idx)
            all_chains_tips_and_middle_vert += single_unselected_verts
            all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
            # The tip of the selected vertices nearest to the first point of the first sketched stroke
            nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = \
                    self.shortest_distance(
                                self.main_object,
                                first_sketched_point_first_stroke_co,
                                all_chains_tips_and_middle_vert
                                )
            # If the nearest tip is not from a closed selection, get the opposite tip vertex index
            if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
               nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:

                nearest_tip_to_first_st_first_pt_opposite_idx = \
                    self.opposite_tip(
                                nearest_tip_to_first_st_first_pt_idx,
                                verts_tips_same_chain_idx
                                )
            # The tip of the selected vertices nearest to the last point of the first sketched stroke
            nearest_tip_to_first_st_last_pt_idx, _temp_dist = \
                    self.shortest_distance(
                                self.main_object,
                                last_sketched_point_first_stroke_co,
                                all_chains_tips_and_middle_vert
                                )
            # The tip of the selected vertices nearest to the first point of the last sketched stroke
            nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = \
                    self.shortest_distance(
                                self.main_object,
                                first_sketched_point_last_stroke_co,
                                all_chains_tips_and_middle_vert
                                )
            if len(self.main_splines.data.splines) > 1:
                # The selected vertex nearest to the first point of the second sketched stroke
                # (This will be useful to determine the direction of the closed
                # selection V when extruding along strokes)
                nearest_vert_to_second_st_first_pt_idx, _temp_dist = \
                        self.shortest_distance(
                                self.main_object,
                                first_sketched_point_second_stroke_co,
                                all_verts_idx
                                )
                # The selected vertex nearest to the first point of the second sketched stroke
                # (This will be useful to determine the direction of the closed
                # selection V2 when extruding along strokes)
                nearest_vert_to_second_st_last_pt_idx, _temp_dist = \
                        self.shortest_distance(
                                self.main_object,
                                last_sketched_point_second_stroke_co,
                                all_verts_idx
                                )
            # Determine if the single selection will be treated as U or as V
            edges_sum = 0
            for i in all_selected_edges_idx:
                edges_sum += (
                    (self.main_object.matrix_world @
                     self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) -
                    (self.main_object.matrix_world @
                     self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)
                    ).length
            average_edge_length = edges_sum / len(all_selected_edges_idx)
            # Get shortest distance from the first point of the last stroke to any participating vertex
            _temp_idx, shortest_distance_to_last_stroke = \
                        self.shortest_distance(
                                self.main_object,
                                first_sketched_point_last_stroke_co,
                                all_participating_verts
                                )
            # If the beginning of the first stroke is near enough, and its orientation
            # difference with the first edge of the nearest selection chain is not too high,
            # interpret things as an "extrude along strokes" instead of "extrude through strokes"
            if shortest_distance_to_first_stroke < average_edge_length / 4 and \
             shortest_distance_to_last_stroke < average_edge_length and \
             len(self.main_splines.data.splines) > 1:
                self.selection_U_exists = False
                self.selection_V_exists = True
                # If the first selection is not closed
                if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
                  nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
                    self.selection_V_is_closed = False
                    closing_vert_U_idx = None
                    closing_vert_U2_idx = None
                    closing_vert_V_idx = None
                    closing_vert_V2_idx = None
                    first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
                    if selection_type == "TWO_NOT_CONNECTED":
                        self.selection_V2_exists = True
                        first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
                else:
                    self.selection_V_is_closed = True
                    closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
                    # Get the neighbors of the first (unselected) vert of the closed selection U.
                    vert_neighbors = []
                    for verts in single_unselected_verts_and_neighbors:
                        if verts[0] == nearest_tip_to_first_st_first_pt_idx:
                            vert_neighbors.append(verts[1])
                            vert_neighbors.append(verts[2])
                            break
                    verts_V = self.get_ordered_verts(
                                    self.main_object, all_selected_edges_idx,
                                    all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
                                    )
                    for i in range(0, len(verts_V)):
                        if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
                            # If the vertex nearest to the first point of the second stroke
                            # is in the first half of the selected verts
                            if i >= len(verts_V) / 2:
                                first_vert_V_idx = vert_neighbors[1]
                                break
                            else:
                                first_vert_V_idx = vert_neighbors[0]
                                break
                if selection_type == "TWO_NOT_CONNECTED":
                    self.selection_V2_exists = True
                    # If the second selection is not closed
                    if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or \
                      nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx:
                        self.selection_V2_is_closed = False
                        closing_vert_V2_idx = None
                        first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
                    else:
                        self.selection_V2_is_closed = True
                        closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
                        # Get the neighbors of the first (unselected) vert of the closed selection U
                        vert_neighbors = []
                        for verts in single_unselected_verts_and_neighbors:
                            if verts[0] == nearest_tip_to_first_st_last_pt_idx:
                                vert_neighbors.append(verts[1])
                                vert_neighbors.append(verts[2])
                                break
                        verts_V2 = self.get_ordered_verts(
                                        self.main_object, all_selected_edges_idx,
                                        all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
                                        )
                        for i in range(0, len(verts_V2)):
                            if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
                                # If the vertex nearest to the first point of the second stroke
                                # is in the first half of the selected verts
                                if i >= len(verts_V2) / 2:
                                    first_vert_V2_idx = vert_neighbors[1]
                                    break
                                else:
                                    first_vert_V2_idx = vert_neighbors[0]
                                    break
                else:
                    self.selection_V2_exists = False
            else:
                self.selection_U_exists = True
                self.selection_V_exists = False
                # If the first selection is not closed
                if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
                  nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
                    self.selection_U_is_closed = False
                    closing_vert_U_idx = None
                    points_tips = []
                    points_tips.append(
                            self.main_object.matrix_world @
                            self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
                            )
                    points_tips.append(
                            self.main_object.matrix_world @
                            self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co
                            )
                    points_first_stroke_tips = []
                    points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
                    points_first_stroke_tips.append(
                        self.main_splines.data.splines[0].bezier_points[
                                                len(self.main_splines.data.splines[0].bezier_points) - 1
                                                ].co
                        )
                    vec_A = points_tips[0] - points_tips[1]
                    vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
                    # Compare the direction of the selection and the first
                    # grease pencil stroke to determine which is the "first" vertex of the selection
                    if vec_A.dot(vec_B) < 0:
                        first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
                    else:
                        first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
                else:
                    self.selection_U_is_closed = True
                    closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
                    # Get the neighbors of the first (unselected) vert of the closed selection U
                    vert_neighbors = []
                    for verts in single_unselected_verts_and_neighbors:
                        if verts[0] == nearest_tip_to_first_st_first_pt_idx:
                            vert_neighbors.append(verts[1])
                            vert_neighbors.append(verts[2])
                            break
                    points_first_and_neighbor = []
                    points_first_and_neighbor.append(
                            self.main_object.matrix_world @
                            self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
                            )
                    points_first_and_neighbor.append(
                            self.main_object.matrix_world @
                            self.main_object.data.vertices[vert_neighbors[0]].co
                            )
                    points_first_stroke_tips = []
                    points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
                    points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
                    vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
                    vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
                    # Compare the direction of the selection and the first grease pencil stroke to
                    # determine which is the vertex neighbor to the first vertex (unselected) of
                    # the closed selection. This will determine the direction of the closed selection
                    if vec_A.dot(vec_B) < 0:
                        first_vert_U_idx = vert_neighbors[1]
                    else:
                        first_vert_U_idx = vert_neighbors[0]
                if selection_type == "TWO_NOT_CONNECTED":
                    self.selection_U2_exists = True
                    # If the second selection is not closed
                    if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or \
                      nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx:
                        self.selection_U2_is_closed = False
                        closing_vert_U2_idx = None
                        first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
                    else:
                        self.selection_U2_is_closed = True
                        closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
                        # Get the neighbors of the first (unselected) vert of the closed selection U
                        vert_neighbors = []
                        for verts in single_unselected_verts_and_neighbors:
                            if verts[0] == nearest_tip_to_last_st_first_pt_idx:
                                vert_neighbors.append(verts[1])
                                vert_neighbors.append(verts[2])
                                break
                        points_first_and_neighbor = []
                        points_first_and_neighbor.append(
                                self.main_object.matrix_world @
                                self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co
                                )
                        points_first_and_neighbor.append(
                                self.main_object.matrix_world @
                                self.main_object.data.vertices[vert_neighbors[0]].co
                                )
                        points_last_stroke_tips = []
                        points_last_stroke_tips.append(
                                self.main_splines.data.splines[
                                                        len(self.main_splines.data.splines) - 1
                                                        ].bezier_points[0].co
                                )
                        points_last_stroke_tips.append(
                                self.main_splines.data.splines[
                                                        len(self.main_splines.data.splines) - 1
                                                        ].bezier_points[1].co
                                )
                        vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
                        vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
                        # Compare the direction of the selection and the last grease pencil stroke to
                        # determine which is the vertex neighbor to the first vertex (unselected) of
                        # the closed selection. This will determine the direction of the closed selection
                        if vec_A.dot(vec_B) < 0:
                            first_vert_U2_idx = vert_neighbors[1]
                        else:
                            first_vert_U2_idx = vert_neighbors[0]
                else:
                    self.selection_U2_exists = False
        elif selection_type == "NO_SELECTION":
            self.selection_U_exists = False
            self.selection_V_exists = False
        # Get an ordered list of the vertices of Selection-U
        verts_ordered_U = []
        if self.selection_U_exists:
            verts_ordered_U = self.get_ordered_verts(
                                    self.main_object, all_selected_edges_idx,
                                    all_verts_idx, first_vert_U_idx,
                                    middle_vertex_idx, closing_vert_U_idx
                                    )
        # Get an ordered list of the vertices of Selection-U2
        verts_ordered_U2 = []
        if self.selection_U2_exists:
            verts_ordered_U2 = self.get_ordered_verts(
                                    self.main_object, all_selected_edges_idx,
                                    all_verts_idx, first_vert_U2_idx,
                                    middle_vertex_idx, closing_vert_U2_idx
                                    )
        # Get an ordered list of the vertices of Selection-V
        verts_ordered_V = []
        if self.selection_V_exists:
            verts_ordered_V = self.get_ordered_verts(
                                    self.main_object, all_selected_edges_idx,
                                    all_verts_idx, first_vert_V_idx,
                                    middle_vertex_idx, closing_vert_V_idx
                                    )
            verts_ordered_V_indices = [x.index for x in verts_ordered_V]
        # Get an ordered list of the vertices of Selection-V2
        verts_ordered_V2 = []
        if self.selection_V2_exists:
            verts_ordered_V2 = self.get_ordered_verts(
                                    self.main_object, all_selected_edges_idx,
                                    all_verts_idx, first_vert_V2_idx,
                                    middle_vertex_idx, closing_vert_V2_idx
                                    )
        # Check if when there are two-not-connected selections both have the same
        # number of verts. If not terminate the script
        if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or
           (self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
            # Display a warning
            self.report({'WARNING'}, "Both selections must have the same number of edges")
            self.stopping_errors = True
            return{'CANCELLED'}
        # Calculate edges U proportions
        # Sum selected edges U lengths
        edges_lengths_U = []
        edges_lengths_sum_U = 0
        if self.selection_U_exists:
            edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(
                                                            self.main_object,
                                                            verts_ordered_U
                                                            )
        if self.selection_U2_exists:
            edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(
                                                            self.main_object,
                                                            verts_ordered_U2
                                                            )
        # Sum selected edges V lengths
        edges_lengths_V = []
        edges_lengths_sum_V = 0
        if self.selection_V_exists:
            edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(
                                                            self.main_object,
                                                            verts_ordered_V
                                                            )
        if self.selection_V2_exists:
            edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(
                                                            self.main_object,
                                                            verts_ordered_V2
                                                            )
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        bpy.ops.curve.subdivide('INVOKE_REGION_WIN',
                                number_cuts=bpy.context.scene.bsurfaces.SURFSK_precision)
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        # Proportions U
        edges_proportions_U = []
        edges_proportions_U = self.get_edges_proportions(
                                    edges_lengths_U, edges_lengths_sum_U,
                                    self.selection_U_exists, self.edges_U
                                    )
        verts_count_U = len(edges_proportions_U) + 1
        if self.selection_U2_exists:
            edges_proportions_U2 = []
            edges_proportions_U2 = self.get_edges_proportions(
                                    edges_lengths_U2, edges_lengths_sum_U2,
                                    self.selection_U2_exists, self.edges_V
                                    )
        # Proportions V
        edges_proportions_V = []
        edges_proportions_V = self.get_edges_proportions(
                                    edges_lengths_V, edges_lengths_sum_V,
                                    self.selection_V_exists, self.edges_V
                                    )
        if self.selection_V2_exists:
            edges_proportions_V2 = []
            edges_proportions_V2 = self.get_edges_proportions(
                                    edges_lengths_V2, edges_lengths_sum_V2,
                                    self.selection_V2_exists, self.edges_V
                                    )
        # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
        # the actual sketched curves with a "closing segment"
        if self.cyclic_follow and not self.selection_V_exists and not \
          ((self.selection_U_exists and not self.selection_U_is_closed) or
          (self.selection_U2_exists and not self.selection_U2_is_closed)):
            simplified_spline_coords = []
            simplified_curve = []
            ob_simplified_curve = []
            splines_first_v_co = []
            for i in range(len(self.main_splines.data.splines)):
                # Create a curve object for the actual spline "cyclic extension"
                simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
                ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
                bpy.context.collection.objects.link(ob_simplified_curve[i])
                simplified_curve[i].dimensions = "3D"
                spline_coords = []
                for bp in self.main_splines.data.splines[i].bezier_points:
                    spline_coords.append(bp.co)
                # Simplification
                simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
                # Get the coordinates of the first vert of the actual spline
                splines_first_v_co.append(simplified_spline_coords[i][0])
                # Generate the spline
                spline = simplified_curve[i].splines.new('BEZIER')
                # less one because one point is added when the spline is created
                spline.bezier_points.add(len(simplified_spline_coords[i]) - 1)
                for p in range(0, len(simplified_spline_coords[i])):
                    spline.bezier_points[p].co = simplified_spline_coords[i][p]
                spline.use_cyclic_u = True
                spline_bp_count = len(spline.bezier_points)
                bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
                ob_simplified_curve[i].select_set(True)
                bpy.context.view_layer.objects.active = ob_simplified_curve[i]
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
                bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
                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')
                # Select the "closing segment", and subdivide it
                ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
                ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
                ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
                ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
                ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
                ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
                segments = sqrt(
                          (ob_simplified_curve[i].data.splines[0].bezier_points[0].co -
                           ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length /
                          self.average_gp_segment_length
                        )
                for t in range(2):
                    bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=int(segments))
                # Delete the other vertices and make it non-cyclic to
                # keep only the needed verts of the "closing segment"
                bpy.ops.curve.select_all(action='INVERT')
                bpy.ops.curve.delete(type='VERT')
                ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
                # Add the points of the "closing segment" to the original curve from grease pencil stroke
                first_new_index = len(self.main_splines.data.splines[i].bezier_points)
                self.main_splines.data.splines[i].bezier_points.add(
                                                    len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1
                                                    )
                for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
                    self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = \
                            ob_simplified_curve[i].data.splines[0].bezier_points[t].co
                # Delete the temporal curve
                bpy.ops.object.delete({"selected_objects": [ob_simplified_curve[i]]})
        # Get the coords of the points distributed along the sketched strokes,
        # with proportions-U of the first selection
        pts_on_strokes_with_proportions_U = self.distribute_pts(
                                                    self.main_splines.data.splines,
                                                    edges_proportions_U
                                                    )
        sketched_splines_parsed = []
        if self.selection_U2_exists:
            # Initialize the multidimensional list with the proportions of all the segments
            proportions_loops_crossing_strokes = []
            for i in range(len(pts_on_strokes_with_proportions_U)):
                proportions_loops_crossing_strokes.append([])
                for t in range(len(pts_on_strokes_with_proportions_U[0])):
                    proportions_loops_crossing_strokes[i].append(None)
            # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
            for lp in range(len(pts_on_strokes_with_proportions_U[0])):
                loop_segments_lengths = []
                for st in range(len(pts_on_strokes_with_proportions_U)):
Brecht Van Lommel's avatar
Brecht Van Lommel committed
                    # When on the first stroke, add the segment from the selection to the first stroke
                    if st == 0:
                        loop_segments_lengths.append(
                                    ((self.main_object.matrix_world @ verts_ordered_U[lp].co) -
                                    pts_on_strokes_with_proportions_U[0][lp]).length
                                    )
                    # For all strokes except for the last, calculate the distance
                    # from the actual stroke to the next
                    if st != len(pts_on_strokes_with_proportions_U) - 1:
                        loop_segments_lengths.append(
                                    (pts_on_strokes_with_proportions_U[st][lp] -
                                    pts_on_strokes_with_proportions_U[st + 1][lp]).length
                                    )
                    # When on the last stroke, add the segments
                    # from the last stroke to the second selection
                    if st == len(pts_on_strokes_with_proportions_U) - 1:
                        loop_segments_lengths.append(
                                    (pts_on_strokes_with_proportions_U[st][lp] -
                                    (self.main_object.matrix_world @ verts_ordered_U2[lp].co)).length
                                    )
                # Calculate full loop length
                loop_seg_lengths_sum = 0
                for i in range(len(loop_segments_lengths)):
                    loop_seg_lengths_sum += loop_segments_lengths[i]
                # Fill the multidimensional list with the proportions of all the segments
                for st in range(len(pts_on_strokes_with_proportions_U)):
                    proportions_loops_crossing_strokes[st][lp] = \
                        loop_segments_lengths[st] / loop_seg_lengths_sum
            # Calculate proportions for each stroke
            for st in range(len(pts_on_strokes_with_proportions_U)):
                actual_stroke_spline = []
                # Needs to be a list for the "distribute_pts" method
                actual_stroke_spline.append(self.main_splines.data.splines[st])
                # Calculate the proportions for the actual stroke.
                actual_edges_proportions_U = []
                for i in range(len(edges_proportions_U)):
                    proportions_sum = 0
                    # Sum the proportions of this loop up to the actual.
                    for t in range(0, st + 1):
                        proportions_sum += proportions_loops_crossing_strokes[t][i]
                    # i + 1, because proportions_loops_crossing_strokes refers to loops,
                    # and the proportions refer to edges, so we start at the element 1
                    # of proportions_loops_crossing_strokes instead of element 0
                    actual_edges_proportions_U.append(
                            edges_proportions_U[i] -
                            ((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)
                            )
                points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
                sketched_splines_parsed.append(points_actual_spline[0])
        else:
            sketched_splines_parsed = pts_on_strokes_with_proportions_U
        # If the selection type is "TWO_NOT_CONNECTED" replace the
        # points of the last spline with the points in the "target" selection
        if selection_type == "TWO_NOT_CONNECTED":
            if self.selection_U2_exists:
                for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
                    sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = \
                            self.main_object.matrix_world @ verts_ordered_U2[i].co
        # Create temporary curves along the "control-points" found
        # on the sketched curves and the mesh selection
        mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
        me = bpy.data.meshes.new(mesh_ctrl_pts_name)
        ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
        ob_ctrl_pts.data = me
        bpy.context.collection.objects.link(ob_ctrl_pts)
        cyclic_loops_U = []
        first_verts = []
        second_verts = []
        last_verts = []
        for i in range(0, verts_count_U):
            vert_num_in_spline = 1
            if self.selection_U_exists:
                ob_ctrl_pts.data.vertices.add(1)
                last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
                last_v.co = self.main_object.matrix_world @ verts_ordered_U[i].co
                vert_num_in_spline += 1
            for t in range(0, len(sketched_splines_parsed)):
                ob_ctrl_pts.data.vertices.add(1)
                v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
                v.co = sketched_splines_parsed[t][i]
                if vert_num_in_spline > 1:
                    ob_ctrl_pts.data.edges.add(1)
                    ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = \
                            len(ob_ctrl_pts.data.vertices) - 2
                    ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = \
                            len(ob_ctrl_pts.data.vertices) - 1
                if t == 0:
                    first_verts.append(v.index)
                if t == 1:
                    second_verts.append(v.index)
                if t == len(sketched_splines_parsed) - 1:
                    last_verts.append(v.index)
                last_v = v
                vert_num_in_spline += 1
        bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
        ob_ctrl_pts.select_set(True)
        bpy.context.view_layer.objects.active = ob_ctrl_pts
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        bpy.ops.mesh.select_all(action='DESELECT')
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        # Determine which loops-U will be "Cyclic"
        for i in range(0, len(first_verts)):
            # When there is Cyclic Cross there is no need of
            # Automatic Join, (and there are at least three strokes)
            if self.automatic_join and not self.cyclic_cross and \
               selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3:
                v = ob_ctrl_pts.data.vertices
                first_point_co = v[first_verts[i]].co
                second_point_co = v[second_verts[i]].co
                last_point_co = v[last_verts[i]].co
                # Coordinates of the point in the center of both the first and last verts.
                verts_center_co = [
                        (first_point_co[0] + last_point_co[0]) / 2,
                        (first_point_co[1] + last_point_co[1]) / 2,
                        (first_point_co[2] + last_point_co[2]) / 2
                        ]
                vec_A = second_point_co - first_point_co
                vec_B = second_point_co - Vector(verts_center_co)
                # Calculate the length of the first segment of the loop,
                # and the length it would have after moving the first vert
                # to the middle position between first and last
                length_original = (second_point_co - first_point_co).length
                length_target = (second_point_co - Vector(verts_center_co)).length
                angle = vec_A.angle(vec_B) / pi
                # If the target length doesn't stretch too much, and the
                # its angle doesn't change to much either
                if length_target <= length_original * 1.03 * self.join_stretch_factor and \
                   angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists:
                    cyclic_loops_U.append(True)
                    # Move the first vert to the center coordinates
                    ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
                    # Select the last verts from Cyclic loops, for later deletion all at once
                else:
                    cyclic_loops_U.append(False)
            else:
                # If "Cyclic Cross" is active then "all" crossing curves become cyclic
                if self.cyclic_cross and not self.selection_U_exists and not \
                   ((self.selection_V_exists and not self.selection_V_is_closed) or
                   (self.selection_V2_exists and not self.selection_V2_is_closed)):

                    cyclic_loops_U.append(True)
                else:
                    cyclic_loops_U.append(False)
        # The cyclic_loops_U list needs to be reversed.
        cyclic_loops_U.reverse()
        # Delete the previously selected (last_)verts.
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        # Create curves from control points.
        bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
        ob_curves_surf = bpy.context.view_layer.objects.active
        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')
        # Make Cyclic the splines designated as Cyclic.
        for i in range(0, len(cyclic_loops_U)):
            ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
        # Get the coords of all points on first loop-U, for later comparison with its
        # subdivided version, to know which points of the loops-U are crossed by the
        # original strokes. The indices will be the same for the other loops-U
        if self.loops_on_strokes:
            coords_loops_U_control_points = []
            for p in ob_ctrl_pts.data.splines[0].bezier_points:
                coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
            tuple(coords_loops_U_control_points)
        # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
        if self.loops_on_strokes and not self.selection_V_exists:
                edges_V_count = len(self.main_splines.data.splines) * self.edges_V
        else:
            edges_V_count = len(edges_proportions_V)
        # The Follow precision will vary depending on the number of Follow face-loops
        precision_multiplier = round(2 + (edges_V_count / 15))
        curve_cuts = bpy.context.scene.bsurfaces.SURFSK_precision * precision_multiplier
        # Subdivide the curves
        bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=curve_cuts)
        # The verts position shifting that happens with splines subdivision.
        # For later reorder splines points
        verts_position_shift = curve_cuts + 1
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        # Reorder coordinates of the points of each spline to put the first point of
        # the spline starting at the position it was the first point before sudividing
        # the curve. And make a new curve object per spline (to handle memory better later)
        splines_U_objects = []
        for i in range(len(ob_curves_surf.data.splines)):
            spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
            ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
            bpy.context.collection.objects.link(ob_spline_U)
            spline_U_curve.dimensions = "3D"
            # Add points to the spline in the new curve object
            ob_spline_U.data.splines.new('BEZIER')
            for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
                if cyclic_loops_U[i] is True and not self.selection_U_exists:  # If the loop is cyclic
                    if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
                        point_index = t + verts_position_shift
                    else:
                        point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
                else:
                    point_index = t
                # to avoid adding the first point since it's added when the spline is created
                if t > 0:
                    ob_spline_U.data.splines[0].bezier_points.add(1)
                ob_spline_U.data.splines[0].bezier_points[t].co = \
                        ob_curves_surf.data.splines[i].bezier_points[point_index].co
            if cyclic_loops_U[i] is True and not self.selection_U_exists:  # If the loop is cyclic
                # Add a last point at the same location as the first one
                ob_spline_U.data.splines[0].bezier_points.add(1)
                ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = \
                        ob_spline_U.data.splines[0].bezier_points[0].co
            else:
                ob_spline_U.data.splines[0].use_cyclic_u = False
            splines_U_objects.append(ob_spline_U)
            bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
            ob_spline_U.select_set(True)
            bpy.context.view_layer.objects.active = ob_spline_U
        # When option "Loops on strokes" is active each "Cross" loop will have
        # its own proportions according to where the original strokes "touch" them
        if self.loops_on_strokes:
            # Get the indices of points where the original strokes "touch" loops-U
            points_U_crossed_by_strokes = []
            for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
                bp = splines_U_objects[0].data.splines[0].bezier_points[i]
                if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
                    points_U_crossed_by_strokes.append(i)
            # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
            edge_order_number_for_splines = {}
            if self.selection_V_exists:
                # For two-connected selections add a first hypothetic stroke at the beginning.
                if selection_type == "TWO_CONNECTED":
                    edge_order_number_for_splines[0] = 0
                for i in range(len(self.main_splines.data.splines)):
                    sp = self.main_splines.data.splines[i]
                    v_idx, _dist_temp = self.shortest_distance(
                                                self.main_object,
                                                sp.bezier_points[0].co,
                                                verts_ordered_V_indices
                                                )
                    # Get the position (edges count) of the vert v_idx in the selected chain V
                    edge_idx_in_chain = verts_ordered_V_indices.index(v_idx)

                    # For two-connected selections the strokes go after the
                    # hypothetic stroke added before, so the index adds one per spline
                    if selection_type == "TWO_CONNECTED":
                        spline_number = i + 1
                    else:
                        spline_number = i
                    edge_order_number_for_splines[spline_number] = edge_idx_in_chain
                    # Get the first and last verts indices for later comparison
                    if i == 0:
                        first_v_idx = v_idx
                    elif i == len(self.main_splines.data.splines) - 1:
                        last_v_idx = v_idx
                if self.selection_V_is_closed:
                    # If there is no last stroke on the last vertex (same as first vertex),
                    # add a hypothetic spline at last vert order
                    if first_v_idx != last_v_idx:
                        edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
                                len(verts_ordered_V_indices) - 1
                    else:
                        if self.cyclic_cross:
                            edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
                                    len(verts_ordered_V_indices) - 2
                            edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
                                    len(verts_ordered_V_indices) - 1
                            edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
                                    len(verts_ordered_V_indices) - 1
        # Get the coords of the points distributed along the
        # "crossing curves", with appropriate proportions-V
        surface_splines_parsed = []
        for i in range(len(splines_U_objects)):
            sp_ob = splines_U_objects[i]
            # If "Loops on strokes" option is active, calculate the proportions for each loop-U
            if self.loops_on_strokes:
                # Segments distances from stroke to stroke
                dist = 0
                full_dist = 0
                segments_distances = []
                for t in range(len(sp_ob.data.splines[0].bezier_points)):
                    bp = sp_ob.data.splines[0].bezier_points[t]
                    if t == 0:
                        last_p = bp.co
                    else:
                        actual_p = bp.co
                        dist += (last_p - actual_p).length
                        if t in points_U_crossed_by_strokes:
                            segments_distances.append(dist)
                            full_dist += dist
                        last_p = actual_p
                # Calculate Proportions.
                used_edges_proportions_V = []
                for t in range(len(segments_distances)):
                    if self.selection_V_exists:
                        if t == 0:
                            order_number_last_stroke = 0
                        segment_edges_length_V = 0
                        segment_edges_length_V2 = 0
                        for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
                            segment_edges_length_V += edges_lengths_V[order]
                            if self.selection_V2_exists:
                                segment_edges_length_V2 += edges_lengths_V2[order]
                        for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
                            # Calculate each "sub-segment" (the ones between each stroke) length
                            if self.selection_V2_exists:
                                proportion_sub_seg = (edges_lengths_V2[order] -
                                    ((edges_lengths_V2[order] - edges_lengths_V[order]) /
                                    len(splines_U_objects) * i)) / (segment_edges_length_V2 -
                                    (segment_edges_length_V2 - segment_edges_length_V) /
                                    len(splines_U_objects) * i)

                                sub_seg_dist = segments_distances[t] * proportion_sub_seg
                            else:
                                proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
                                sub_seg_dist = segments_distances[t] * proportion_sub_seg
                            used_edges_proportions_V.append(sub_seg_dist / full_dist)
                        order_number_last_stroke = edge_order_number_for_splines[t + 1]
                        for _c in range(self.edges_V):
                            # Calculate each "sub-segment" (the ones between each stroke) length
                            sub_seg_dist = segments_distances[t] / self.edges_V
                            used_edges_proportions_V.append(sub_seg_dist / full_dist)
                actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
                surface_splines_parsed.append(actual_spline[0])
            else:
                if self.selection_V2_exists:
                    used_edges_proportions_V = []
                    for p in range(len(edges_proportions_V)):
                        used_edges_proportions_V.append(
                                    edges_proportions_V2[p] -
                                    ((edges_proportions_V2[p] -
                                    edges_proportions_V[p]) / len(splines_U_objects) * i)
                                    )
                else:
                    used_edges_proportions_V = edges_proportions_V
                actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
                surface_splines_parsed.append(actual_spline[0])
        # Set the verts of the first and last splines to the locations
        # of the respective verts in the selections
        if self.selection_V_exists:
            for i in range(0, len(surface_splines_parsed[0])):
                surface_splines_parsed[len(surface_splines_parsed) - 1][i] = \
                        self.main_object.matrix_world @ verts_ordered_V[i].co
        if selection_type == "TWO_NOT_CONNECTED":
            if self.selection_V2_exists:
                for i in range(0, len(surface_splines_parsed[0])):
                    surface_splines_parsed[0][i] = self.main_object.matrix_world @ verts_ordered_V2[i].co
        # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
        # merge the verts of the tips of the loops when they are "near enough"
        if self.automatic_join and selection_type != "TWO_CONNECTED":
            # Join the tips of "Follow" loops that are near enough and must be "closed"
            if not self.selection_V_exists and len(edges_proportions_U) >= 3:
                for i in range(len(surface_splines_parsed[0])):
                    sp = surface_splines_parsed
                    loop_segment_dist = (sp[0][i] - sp[1][i]).length
                    verts_middle_position_co = [
                            (sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2,
                            (sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2,
                            (sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2
                            ]
                    points_original = []
                    points_original.append(sp[1][i])
                    points_original.append(sp[0][i])
                    points_target = []
                    points_target.append(sp[1][i])
                    points_target.append(Vector(verts_middle_position_co))
                    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)
        ob_surface = object_utils.object_data_add(context, me_surf)
        ob_surface.location = (0.0, 0.0, 0.0)
        ob_surface.rotation_euler = (0.0, 0.0, 0.0)
        ob_surface.scale = (1.0, 1.0, 1.0)
        # 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'}
        try:
            global global_shade_smooth
            if global_shade_smooth:
                bpy.ops.object.shade_smooth()
            else:
                bpy.ops.object.shade_flat()
            bpy.context.scene.bsurfaces.SURFSK_shade_smooth = global_shade_smooth
        except:
            pass
    def execute(self, context):
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
            global global_mesh_object
            global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
            bpy.data.objects[global_mesh_object].select_set(True)
            self.main_object = bpy.data.objects[global_mesh_object]
            bpy.context.view_layer.objects.active = self.main_object
            bsurfaces_props = bpy.context.scene.bsurfaces
        except:
            self.report({'WARNING'}, "Specify the name of the object with retopology")
            return{"CANCELLED"}
        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.mode_set('INVOKE_REGION_WIN', 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.mode_set('INVOKE_REGION_WIN', 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.editmode_toggle('INVOKE_REGION_WIN')
            if strokes_for_rectangular_surface:
                self.rectangular_surface(context)
            elif strokes_for_crosshatch:
                self.crosshatch_surface_execute(context)
            #Set Shade smooth to new polygons
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
            global global_shade_smooth
            if global_shade_smooth:
                bpy.ops.object.shade_smooth()
            else:
Campbell Barton's avatar
Campbell Barton committed
                bpy.ops.object.shade_flat()
            # Delete main splines
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', 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:
                    bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
            if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors:
                    bpy.context.annotation_data.layers.active.clear()
            bsurfaces_props = bpy.context.scene.bsurfaces
            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('INVOKE_REGION_WIN', 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
            global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
            bpy.data.objects[global_mesh_object].select_set(True)
            self.main_object = bpy.data.objects[global_mesh_object]
            bpy.context.view_layer.objects.active = self.main_object
        except:
            self.report({'WARNING'}, "Specify the name of the object with retopology")
            return{"CANCELLED"}
        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')
        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
                global global_gpencil_object
                gp = bpy.data.objects[global_gpencil_object]
                self.original_curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
                self.using_external_curves = False
            elif self.strokes_type == "GP_ANNOTATION":
                # Convert grease pencil strokes to curve
                gp = bpy.context.annotation_data
                self.original_curve = conver_gpencil_to_curve(self, context, gp, 'Annotation')
                self.using_external_curves = False
            elif self.strokes_type == "EXTERNAL_CURVE":
                global global_curve_object
                self.original_curve = bpy.data.objects[global_curve_object]
                self.using_external_curves = True
            # 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.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]})
            # Set again since "execute()" will turn it again to its initial value
            self.execute(context)
            if not self.stopping_errors:
                # Delete grease pencil strokes
                if self.strokes_type == "GP_STROKES":
                    try:
                        bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
                    except:
                        pass
                # Delete annotation strokes
                elif self.strokes_type == "GP_ANNOTATION":
                    try:
                        bpy.context.annotation_data.layers.active.clear()
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
                bpy.ops.object.delete({"selected_objects": [self.original_curve]})
                bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
                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"}
# ----------------------------
# Init operator
class MESH_OT_SURFSK_init(Operator):
    bl_idname = "mesh.surfsk_init"
    bl_label = "Bsurfaces initialize"
    bl_description = "Add an empty mesh object with useful settings"
    def execute(self, context):
        bs = bpy.context.scene.bsurfaces
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        global global_mesh_object
        global global_gpencil_object
            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)
            mesh_object.select_set(True)
            bpy.context.view_layer.objects.active = mesh_object
            mesh_object.show_all_edges = True
            mesh_object.display_type = 'SOLID'
            mesh_object.show_wire = True
            global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
            if global_shade_smooth:
                bpy.ops.object.shade_smooth()
            else:
                bpy.ops.object.shade_flat()
            color_red = [1.0, 0.0, 0.0, 0.3]
            material = makeMaterial("BSurfaceMesh", color_red)
            modifier = mesh_object.modifiers.new("", 'SHRINKWRAP')
            if self.active_object is not None:
                modifier.target = self.active_object
                modifier.wrap_method = 'TARGET_PROJECT'
                modifier.wrap_mode = 'OUTSIDE_SURFACE'
            global_mesh_object = mesh_object.name
            bpy.context.scene.bsurfaces.SURFSK_mesh = bpy.data.objects[global_mesh_object]
            bpy.context.scene.tool_settings.snap_elements = {'FACE'}
            bpy.context.scene.tool_settings.use_snap = True
            bpy.context.scene.tool_settings.use_snap_self = False
            bpy.context.scene.tool_settings.use_snap_align_rotation = True
            bpy.context.scene.tool_settings.use_snap_project = True
            bpy.context.scene.tool_settings.use_snap_rotate = True
            bpy.context.scene.tool_settings.use_snap_scale = True

            bpy.context.scene.tool_settings.use_mesh_automerge = True
            bpy.context.scene.tool_settings.double_threshold = 0.01
        if context.scene.bsurfaces.SURFSK_guide == 'GPencil' and bs.SURFSK_gpencil == 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('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
            global_gpencil_object = gpencil_object.name
            bpy.context.scene.bsurfaces.SURFSK_gpencil = bpy.data.objects[global_gpencil_object]
            gpencil_object.data.stroke_depth_order = '3D'
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
            bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
        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'

    def invoke(self, context, event):
        if bpy.context.active_object:
            self.active_object = bpy.context.active_object
        else:
            self.active_object = None
# ----------------------------
# Add modifiers operator
class MESH_OT_SURFSK_add_modifiers(Operator):
    bl_idname = "mesh.surfsk_add_modifiers"
    bl_label = "Add Mirror and others modifiers"
    bl_description = "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
    bl_options = {'REGISTER', 'UNDO'}
        bs = bpy.context.scene.bsurfaces
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
            self.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
        else:
            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
                shrinkwrap = next(mod for mod in mesh_object.modifiers
                                  if mod.type == 'SHRINKWRAP')
                shrinkwrap = mesh_object.modifiers.new("", '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'
                shrinkwrap.show_on_cage = True
                shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
                mirror = next(mod for mod in mesh_object.modifiers
                              if mod.type == 'MIRROR')
                mirror = mesh_object.modifiers.new("", 'MIRROR')
            mirror.use_clip = True
                _subsurf = next(mod for mod in mesh_object.modifiers
                                if mod.type == 'SUBSURF')
                _subsurf = mesh_object.modifiers.new("", 'SUBSURF')
                solidify = next(mod for mod in mesh_object.modifiers
                                if mod.type == 'SOLIDIFY')
                solidify = mesh_object.modifiers.new("", '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"}
# ----------------------------
class MESH_OT_SURFSK_edit_surface(Operator):
    bl_idname = "mesh.surfsk_edit_surface"
    bl_label = "Bsurfaces edit surface"
    bl_description = "Edit surface mesh"
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
        bpy.context.scene.bsurfaces.SURFSK_mesh.select_set(True)
        bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_mesh
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
        bpy.ops.wm.tool_set_by_id(name="builtin.select")
    def invoke(self, context, event):
        try:
            global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
            bpy.data.objects[global_mesh_object].select_set(True)
            self.main_object = bpy.data.objects[global_mesh_object]
            bpy.context.view_layer.objects.active = self.main_object
        except:
            self.report({'WARNING'}, "Specify the name of the object with retopology")
            return{"CANCELLED"}
# ----------------------------
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):
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
        bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
        bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_gpencil
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
        bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")

    def invoke(self, context, event):
            bpy.context.scene.bsurfaces.SURFSK_gpencil.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"
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
        gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil
        gpencil_object.select_set(True)
        bpy.context.view_layer.objects.active = gpencil_object
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT_GPENCIL')
        try:
            bpy.ops.gpencil.select_all(action='SELECT')
        except:
            pass

    def invoke(self, context, event):
        try:
           bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
        except:
            self.report({'WARNING'}, "Specify the name of the object with strokes")
            return{"CANCELLED"}

        self.execute(context)

        return {"FINISHED"}

# ----------------------------
# Convert annotation to curves operator
class GPENCIL_OT_SURFSK_annotation_to_curves(Operator):
    bl_idname = "gpencil.surfsk_annotations_to_curves"
    bl_label = "Convert annotation to curves"
    bl_description = "Convert annotation to curves for editing"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        curve = conver_gpencil_to_curve(self, context, None, 'Annotation')
        if curve != None:
            # Delete annotation strokes
            try:
                bpy.context.annotation_data.layers.active.clear()
            # Clean up curves
            curve.select_set(True)
            bpy.context.view_layer.objects.active = curve
            bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
            strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
            _strokes_num = len(strokes)
        except:
            self.report({'WARNING'}, "Not active annotation")
            return{"CANCELLED"}
# ----------------------------
# Convert strokes to curves operator
class GPENCIL_OT_SURFSK_strokes_to_curves(Operator):
    bl_idname = "gpencil.surfsk_strokes_to_curves"
    bl_label = "Convert strokes to curves"
    bl_description = "Convert grease pencil strokes to curves for editing"
    bl_options = {'REGISTER', 'UNDO'}
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        # Convert grease pencil strokes to curve
        gp = bpy.context.scene.bsurfaces.SURFSK_gpencil
        curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
        if curve != None:
            # Delete grease pencil strokes
            try:
                bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
            except:
                pass
            curve.select_set(True)
            bpy.context.view_layer.objects.active = curve
            bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
    def invoke(self, context, event):
           bpy.context.scene.bsurfaces.SURFSK_gpencil.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"}
# ----------------------------
# Edit curve operator
class CURVE_OT_SURFSK_edit_curve(Operator):
    bl_idname = "curve.surfsk_edit_curve"
    bl_label = "Bsurfaces edit curve"
    bl_description = "Edit curve"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
        bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
        bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_curve
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
    def invoke(self, context, event):
        try:
            bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
        except:
            self.report({'WARNING'}, "Specify the name of the object with curve")
            return{"CANCELLED"}
# ----------------------------
# Reorder splines
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
        shrinkwrap = curves_duplicate_2.modifiers.new("", 'SHRINKWRAP')
        shrinkwrap.wrap_method = "NEAREST_VERTEX"
        shrinkwrap.target = GP_strokes_mesh
        bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier=shrinkwrap.name)
        # 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')
            bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
        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")
            self.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
        return {"FINISHED"}
# ----------------------------
# Set first points operator
class CURVE_OT_SURFSK_first_points(Operator):
    bl_idname = "curve.surfsk_first_points"
    bl_label = "Bsurfaces set first points"
    bl_description = "Set the selected points as the first point of each spline"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        splines_to_invert = []
        # Check non-cyclic splines to invert
        for i in range(len(self.main_curve.data.splines)):
            b_points = self.main_curve.data.splines[i].bezier_points
            if i not in self.cyclic_splines:  # Only for non-cyclic splines
                if b_points[len(b_points) - 1].select_control_point:
                    splines_to_invert.append(i)
        # Reorder points of cyclic splines, and set all handles to "Automatic"
        # Check first selected point
        cyclic_splines_new_first_pt = {}
        for i in self.cyclic_splines:
            sp = self.main_curve.data.splines[i]
            for t in range(len(sp.bezier_points)):
                bp = sp.bezier_points[t]
                if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
                    cyclic_splines_new_first_pt[i] = t
                    break  # To take only one if there are more
        for spline_idx in cyclic_splines_new_first_pt:
            sp = self.main_curve.data.splines[spline_idx]
            spline_old_coords = []
            for bp_old in sp.bezier_points:
                coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
                left_handle_type = str(bp_old.handle_left_type)
                left_handle_length = float(bp_old.handle_left.length)
                left_handle_xyz = (
                        float(bp_old.handle_left.x),
                        float(bp_old.handle_left.y),
                        float(bp_old.handle_left.z)
                        )
                right_handle_type = str(bp_old.handle_right_type)
                right_handle_length = float(bp_old.handle_right.length)
                right_handle_xyz = (
                        float(bp_old.handle_right.x),
                        float(bp_old.handle_right.y),
                        float(bp_old.handle_right.z)
                        )
                spline_old_coords.append(
                        [coords, left_handle_type,
                        right_handle_type, left_handle_length,
                        right_handle_length, left_handle_xyz,
                        right_handle_xyz]
                        )
            for t in range(len(sp.bezier_points)):
                bp = sp.bezier_points
                if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
                    new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
                else:
                    new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
                bp[t].co = Vector(spline_old_coords[new_index][0])
                bp[t].handle_left.length = spline_old_coords[new_index][3]
                bp[t].handle_right.length = spline_old_coords[new_index][4]
                bp[t].handle_left_type = "FREE"
                bp[t].handle_right_type = "FREE"
                bp[t].handle_left.x = spline_old_coords[new_index][5][0]
                bp[t].handle_left.y = spline_old_coords[new_index][5][1]
                bp[t].handle_left.z = spline_old_coords[new_index][5][2]
                bp[t].handle_right.x = spline_old_coords[new_index][6][0]
                bp[t].handle_right.y = spline_old_coords[new_index][6][1]
                bp[t].handle_right.z = spline_old_coords[new_index][6][2]
                bp[t].handle_left_type = spline_old_coords[new_index][1]
                bp[t].handle_right_type = spline_old_coords[new_index][2]
        # Invert the non-cyclic splines designated above
        for i in range(len(splines_to_invert)):
            bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
            bpy.ops.curve.switch_direction()
        bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
        # Keep selected the first vert of each spline
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        for i in range(len(self.main_curve.data.splines)):
            if not self.main_curve.data.splines[i].use_cyclic_u:
                bp = self.main_curve.data.splines[i].bezier_points[0]
            else:
                bp = self.main_curve.data.splines[i].bezier_points[
                                                        len(self.main_curve.data.splines[i].bezier_points) - 1
                                                        ]
            bp.select_control_point = True
            bp.select_right_handle = True
            bp.select_left_handle = True
        bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
        return {'FINISHED'}
    def invoke(self, context, event):
        self.main_curve = bpy.context.object
        # Check if all curves are Bezier, and detect which ones are cyclic
        self.cyclic_splines = []
        for i in range(len(self.main_curve.data.splines)):
            if self.main_curve.data.splines[i].type != "BEZIER":
                self.report({'WARNING'}, "All splines must be Bezier type")
                return {'CANCELLED'}
            else:
                if self.main_curve.data.splines[i].use_cyclic_u:
                    self.cyclic_splines.append(i)
        self.execute(context)
        self.report({'INFO'}, "First points have been set")

# Add-ons Preferences Update Panel

# Define Panel classes for updating
panels = (
        VIEW3D_PT_tools_SURFSK_mesh,
        VIEW3D_PT_tools_SURFSK_curve
def conver_gpencil_to_curve(self, context, pencil, type):
    newCurve = bpy.data.curves.new(type + '_curve', type='CURVE')
    newCurve.dimensions = '3D'
    CurveObject = object_utils.object_data_add(context, newCurve)
        try:
            strokes = pencil.data.layers.active.active_frame.strokes
        except:
            error = True
        CurveObject.location = pencil.location
        CurveObject.rotation_euler = pencil.rotation_euler
        CurveObject.scale = pencil.scale
            strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
        CurveObject.location = (0.0, 0.0, 0.0)
        CurveObject.rotation_euler = (0.0, 0.0, 0.0)
        CurveObject.scale = (1.0, 1.0, 1.0)
        for i, _stroke in enumerate(strokes):
Campbell Barton's avatar
Campbell Barton committed
            data_list = [ (point.co.x, point.co.y, point.co.z)
                            for point in stroke_points ]
            points_to_add = len(data_list)-1
            flat_list = []
            for point in data_list:
                flat_list.extend(point)
            spline = newCurve.splines.new(type='BEZIER')
            spline.bezier_points.add(points_to_add)
            spline.bezier_points.foreach_set("co", flat_list)
            for point in spline.bezier_points:
                point.handle_left_type="AUTO"
                point.handle_right_type="AUTO"

def update_panel(self, context):
    message = "Bsurfaces GPL Edition: Updating Panel locations has failed"
    try:
        for panel in panels:
            if "bl_rna" in panel.__dict__:
                bpy.utils.unregister_class(panel)

        for panel in panels:
            category = context.preferences.addons[__name__].preferences.category
            if category != 'Tool':
                panel.bl_category = context.preferences.addons[__name__].preferences.category
            else:
                context.preferences.addons[__name__].preferences.category = 'Edit'
                panel.bl_category = 'Edit'
                raise ValueError("You can not install add-ons in the Tool panel")
            bpy.utils.register_class(panel)

    except Exception as e:
        print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
        pass
def makeMaterial(name, diffuse):

    if name in bpy.data.materials:
        material = bpy.data.materials[name]
        material.diffuse_color = diffuse
    else:
        material = bpy.data.materials.new(name)
        material.diffuse_color = diffuse

    return material

def update_mesh(self, context):
    try:
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        bpy.ops.object.select_all(action='DESELECT')
        bpy.context.view_layer.update()
        global global_mesh_object
        global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
        bpy.data.objects[global_mesh_object].select_set(True)
        bpy.context.view_layer.objects.active = bpy.data.objects[global_mesh_object]
        print("Select mesh object")

def update_gpencil(self, context):
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        bpy.ops.object.select_all(action='DESELECT')
        bpy.context.view_layer.update()
        global global_gpencil_object
        global_gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil.name
        bpy.data.objects[global_gpencil_object].select_set(True)
        bpy.context.view_layer.objects.active = bpy.data.objects[global_gpencil_object]
        print("Select gpencil object")
def update_curve(self, context):
        bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        bpy.ops.object.select_all(action='DESELECT')
        bpy.context.view_layer.update()
        global global_curve_object
        global_curve_object = bpy.context.scene.bsurfaces.SURFSK_curve.name
        bpy.data.objects[global_curve_object].select_set(True)
        bpy.context.view_layer.objects.active = bpy.data.objects[global_curve_object]
def update_shade_smooth(self, context):
    try:
        global global_shade_smooth
        global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
        bpy.ops.object.select_all(action='DESELECT')
        global global_mesh_object
        global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
        bpy.data.objects[global_mesh_object].select_set(True)
        if global_shade_smooth:
            bpy.ops.object.shade_smooth()
        else:
            bpy.ops.object.shade_flat()
        if contex_mode == "EDIT_MESH":
            bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
class BsurfPreferences(AddonPreferences):
    # this must match the addon name, use '__package__'
    # when defining this in a submodule of a python package.
    bl_idname = __name__
    category: StringProperty(
            name="Tab Category",
            description="Choose a name for the category of the panel",
            update=update_panel
            )

    def draw(self, context):
        layout = self.layout
        row = layout.row()
        col = row.column()
        col.label(text="Tab Category:")
        col.prop(self, "category", text="")
# Properties
class BsurfacesProps(PropertyGroup):
    SURFSK_guide: EnumProperty(
        name="Guide:",
        items=[
                ('Annotation', 'Annotation', 'Annotation'),
                ('GPencil', 'GPencil', 'GPencil'),
                ('Curve', 'Curve', 'Curve')
              ],
        default="Annotation"
        )
    SURFSK_edges_U: IntProperty(
                    name="Cross",
                    description="Number of face-loops crossing the strokes",
                    default=5,
                    min=1,
                    max=200
                    )
    SURFSK_edges_V: IntProperty(
                    name="Follow",
                    description="Number of face-loops following the strokes",
                    default=1,
                    min=1,
                    max=200
                    )
    SURFSK_cyclic_cross: BoolProperty(
                name="Cyclic Cross",
                description="Make cyclic the face-loops crossing the strokes",
                default=False
                )
    SURFSK_cyclic_follow: BoolProperty(
                name="Cyclic Follow",
                description="Make cyclic the face-loops following the strokes",
                default=False
                )
    SURFSK_keep_strokes: BoolProperty(
                name="Keep strokes",
                description="Keeps the sketched strokes or curves after adding the surface",
                default=False
                )
    SURFSK_automatic_join: BoolProperty(
                name="Automatic join",
                description="Join automatically vertices of either surfaces "
                            "generated by crosshatching, or from the borders of closed shapes",
                default=True
                )
    SURFSK_loops_on_strokes: BoolProperty(
                name="Loops on strokes",
                description="Make the loops match the paths of the strokes",
                default=True
                )
    SURFSK_precision: IntProperty(
                name="Precision",
                description="Precision level of the surface calculation",
                default=2,
                min=1,
                max=100
                )
                type=bpy.types.Object,
                description="Mesh of BSurface",
                update=update_mesh,
                )
    SURFSK_gpencil: PointerProperty(
                name="GreasePencil object",
                type=bpy.types.Object,
                description="GreasePencil object",
                update=update_gpencil,
    SURFSK_curve: PointerProperty(
                name="Curve object",
                description="Curve object",
                update=update_curve,
    SURFSK_shade_smooth: BoolProperty(
                name="Shade smooth",
                description="Render and display faces smooth, using interpolated Vertex Normals",
                default=False,
                update=update_shade_smooth,
                )
    MESH_OT_SURFSK_init,
    MESH_OT_SURFSK_add_modifiers,
    MESH_OT_SURFSK_add_surface,
    MESH_OT_SURFSK_edit_surface,
    GPENCIL_OT_SURFSK_add_strokes,
    GPENCIL_OT_SURFSK_edit_strokes,
    GPENCIL_OT_SURFSK_strokes_to_curves,
    GPENCIL_OT_SURFSK_annotation_to_curves,
    GPENCIL_OT_SURFSK_add_annotation,
    CURVE_OT_SURFSK_reorder_splines,
    CURVE_OT_SURFSK_first_points,
    BsurfPreferences,
def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    for panel in panels:
        bpy.utils.register_class(panel)
    bpy.types.Scene.bsurfaces = PointerProperty(type=BsurfacesProps)
    update_panel(None, bpy.context)
def unregister():
    for panel in panels:
        bpy.utils.unregister_class(panel)
    for cls in classes:
        bpy.utils.unregister_class(cls)
    del bpy.types.Scene.bsurfaces

if __name__ == "__main__":
Guillermo S. Romero's avatar
Guillermo S. Romero committed
    register()