Newer
Older
# SPDX-License-Identifier: GPL-2.0-or-later
"author": "Eclectiel, Vladimir Spivak (cwolf3d)",
Pratik Borhade
committed
"version": (1, 8, 1),
"location": "View3D EditMode > Sidebar > Edit Tab",
"description": "Modeling and retopology tool",
"doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
"category": "Mesh",
}
CoDEmanX
committed
from bpy_extras import object_utils
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,
Spivak Vladimir (cwolf3d)
committed
EnumProperty,
Spivak Vladimir (cwolf3d)
committed
FloatVectorProperty,
)
from bpy.types import (
Operator,
Panel,
PropertyGroup,
AddonPreferences,
)
# ----------------------------
Spivak Vladimir (cwolf3d)
committed
# GLOBAL
global_shade_smooth = False
global_mesh_object = ""
global_gpencil_object = ""
global_curve_object = ""
# ----------------------------
# Panels
class VIEW3D_PT_tools_SURFSK_mesh(Panel):
bl_category = 'Edit'
CoDEmanX
committed
def draw(self, context):
layout = self.layout
Vladimir Spivak(cwolf3d)
committed
bs = context.scene.bsurfaces
CoDEmanX
committed
row = layout.row()
row.separator()
Spivak Vladimir (cwolf3d)
committed
col.operator("mesh.surfsk_init", text="Initialize (Add BSurface mesh)")
col.operator("mesh.surfsk_add_modifiers", text="Add Mirror and others modifiers")
Spivak Vladimir (cwolf3d)
committed
col.label(text="Mesh of BSurface:")
Vladimir Spivak(cwolf3d)
committed
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
Vladimir Spivak(cwolf3d)
committed
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
Spivak Vladimir (cwolf3d)
committed
Spivak Vladimir (cwolf3d)
committed
col.label(text="Guide strokes:")
Vladimir Spivak(cwolf3d)
committed
col.row().prop(bs, "SURFSK_guide", expand=True)
if bs.SURFSK_guide == 'GPencil':
col.prop(bs, "SURFSK_gpencil", text="")
col.separator()
Vladimir Spivak(cwolf3d)
committed
if bs.SURFSK_guide == 'Curve':
col.prop(bs, "SURFSK_curve", text="")
col.separator()
Spivak Vladimir (cwolf3d)
committed
Spivak Vladimir (cwolf3d)
committed
col.separator()
col.operator("mesh.surfsk_add_surface", text="Add Surface")
Spivak Vladimir (cwolf3d)
committed
col.operator("mesh.surfsk_edit_surface", text="Edit Surface")
Spivak Vladimir (cwolf3d)
committed
col.separator()
Vladimir Spivak(cwolf3d)
committed
if bs.SURFSK_guide == 'GPencil':
col.operator("gpencil.surfsk_add_strokes", text="Add Strokes")
col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
Spivak Vladimir (cwolf3d)
committed
col.separator()
col.operator("gpencil.surfsk_strokes_to_curves", text="Strokes to curves")
Spivak Vladimir (cwolf3d)
committed
Vladimir Spivak(cwolf3d)
committed
if bs.SURFSK_guide == 'Annotation':
col.operator("gpencil.surfsk_add_annotation", text="Add Annotation")
Spivak Vladimir (cwolf3d)
committed
col.separator()
col.operator("gpencil.surfsk_annotations_to_curves", text="Annotation to curves")
Spivak Vladimir (cwolf3d)
committed
Vladimir Spivak(cwolf3d)
committed
if bs.SURFSK_guide == 'Curve':
Spivak Vladimir (cwolf3d)
committed
col.operator("curve.surfsk_edit_curve", text="Edit curve")
col.separator()
col.label(text="Initial settings:")
Vladimir Spivak(cwolf3d)
committed
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")
Spivak Vladimir (cwolf3d)
committed
class VIEW3D_PT_tools_SURFSK_curve(Panel):
bl_category = 'Edit'
CoDEmanX
committed
@classmethod
def poll(cls, context):
return context.active_object
CoDEmanX
committed
def draw(self, context):
layout = self.layout
CoDEmanX
committed
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")
CoDEmanX
committed
# ----------------------------
def get_strokes_type(context):
strokes_type = "NO_STROKES"
CoDEmanX
committed
# Check if they are annotation
Spivak Vladimir (cwolf3d)
committed
if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
Spivak Vladimir (cwolf3d)
committed
strokes_num = len(strokes)
if strokes_num > 0:
strokes_type = "GP_ANNOTATION"
except:
strokes_type = "NO_STROKES"
Spivak Vladimir (cwolf3d)
committed
# 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
Spivak Vladimir (cwolf3d)
committed
strokes_num = len(strokes)
if strokes_num > 0:
strokes_type = "GP_STROKES"
except:
strokes_type = "NO_STROKES"
CoDEmanX
committed
# 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)
Spivak Vladimir (cwolf3d)
committed
# 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
CoDEmanX
committed
else:
strokes_type = "EXTERNAL_NO_CURVE"
strokes_type = "NO_STROKES"
CoDEmanX
committed
Spivak Vladimir (cwolf3d)
committed
# 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])
Spivak Vladimir (cwolf3d)
committed
Spivak Vladimir (cwolf3d)
committed
# 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
Spivak Vladimir (cwolf3d)
committed
# ----------------------------
Spivak Vladimir (cwolf3d)
committed
class MESH_OT_SURFSK_add_surface(Operator):
bl_idname = "mesh.surfsk_add_surface"
bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
CoDEmanX
committed
Vladimir Spivak(cwolf3d)
committed
is_crosshatch: BoolProperty(
default=False
)
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
)
Spivak Vladimir (cwolf3d)
committed
name="Cross",
description="Number of face-loops crossing the strokes",
default=1,
min=1,
max=200
)
name="Follow",
description="Number of face-loops following the strokes",
default=1,
min=1,
max=200
)
name="Cyclic Cross",
description="Make cyclic the face-loops crossing the strokes",
default=False
)
name="Cyclic Follow",
description="Make cyclic the face-loops following the strokes",
default=False
)
name="Loops on strokes",
description="Make the loops match the paths of the strokes",
default=False
)
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()
CoDEmanX
committed
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
row = layout.row()
CoDEmanX
committed
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()
CoDEmanX
committed
if not self.selection_V_exists:
col.prop(self, "edges_V")
row.separator()
CoDEmanX
committed
CoDEmanX
committed
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)
):
CoDEmanX
committed
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)
):
CoDEmanX
committed
col.prop(self, "loops_on_strokes")
CoDEmanX
committed
col.prop(self, "automatic_join")
if self.automatic_join:
row.separator()
col.separator()
row.separator()
col.prop(self, "join_stretch_factor")
Spivak Vladimir (cwolf3d)
committed
col.prop(self, "keep_strokes")
CoDEmanX
committed
# 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):
if closing_vert_idx is not None:
verts_ordered.append(ob.data.vertices[closing_vert_idx])
CoDEmanX
committed
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]
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
CoDEmanX
committed
if edges_non_matched == len(all_selected_edges_idx):
finish_while = True
CoDEmanX
committed
CoDEmanX
committed
if closing_vert_idx is not None:
verts_ordered.append(ob.data.vertices[closing_vert_idx])
CoDEmanX
committed
if middle_vertex_idx is not None:
verts_ordered.append(ob.data.vertices[middle_vertex_idx])
CoDEmanX
committed
CoDEmanX
committed
# Calculates length of a chain of points.
CoDEmanX
committed
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
CoDEmanX
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]))
CoDEmanX
committed
edges_lengths.append(edge_length)
edges_lengths_sum += edge_length
CoDEmanX
committed
CoDEmanX
committed
return edges_lengths, edges_lengths_sum
CoDEmanX
committed
# 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
CoDEmanX
committed
CoDEmanX
committed
# 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]
CoDEmanX
committed
CoDEmanX
committed
if angle > 0.5 * pi:
angle = abs(angle - pi)
CoDEmanX
committed
CoDEmanX
committed
# 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
CoDEmanX
committed
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
CoDEmanX
committed
if dist < prev_dist:
prev_dist = dist
nearest_vert_idx = verts_idx[i]
shortest_dist = dist
CoDEmanX
committed
return nearest_vert_idx, shortest_dist
CoDEmanX
committed
# 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]
CoDEmanX
committed
CoDEmanX
committed
# 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)
CoDEmanX
committed
simplified_spline.append(spline_coords[0])
for i in range(1, segments_num):
simplified_spline.append(spline_coords[i * points_between_segments])
CoDEmanX
committed
simplified_spline.append(spline_coords[len(spline_coords) - 1])
CoDEmanX
committed
CoDEmanX
committed
# 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
CoDEmanX
committed
CoDEmanX
committed
# 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)
CoDEmanX
committed
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
CoDEmanX
committed
# 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
CoDEmanX
committed
# 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
CoDEmanX
committed
# If the potential is still shorter, keep adding
if potential_segment_length < target_length:
partial_segment_length = potential_segment_length
CoDEmanX
committed
CoDEmanX
committed
# 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)
CoDEmanX
committed
surface_splines_parsed[sp_idx].append(intermediate_co)
CoDEmanX
committed
partial_segment_length += remaining_dist
prev_p_co = intermediate_co
CoDEmanX
committed
CoDEmanX
committed
# 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
CoDEmanX
committed
CoDEmanX
committed
CoDEmanX
committed
surface_splines_parsed[sp_idx].append(
surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
)
CoDEmanX
committed
CoDEmanX
committed
# Counts the number of faces that belong to each edge
def edge_face_count(self, ob):
ed_keys_count_dict = {}
CoDEmanX
committed
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
CoDEmanX
committed
edge_face_count = []
for i in range(len(ob.data.edges)):
edge_face_count.append(0)
CoDEmanX
committed
for i in range(len(ob.data.edges)):
ed = ob.data.edges[i]
CoDEmanX
committed
v1 = ed.vertices[0]
v2 = ed.vertices[1]
CoDEmanX
committed
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)]
CoDEmanX
committed
CoDEmanX
committed
# 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
CoDEmanX
committed
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
CoDEmanX
committed
# 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)
CoDEmanX
committed
CoDEmanX
committed
if not ed.vertices[0] in all_selected_verts:
all_selected_verts.append(ed.vertices[0])
CoDEmanX
committed
if not ed.vertices[1] in all_selected_verts:
all_selected_verts.append(ed.vertices[1])
CoDEmanX
committed
CoDEmanX
committed
# 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
CoDEmanX
committed
# 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
CoDEmanX
committed
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]] = []
CoDEmanX
committed
edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
include_edge = True
CoDEmanX
committed
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]] = []
CoDEmanX
committed
edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
include_edge = True
CoDEmanX
committed
all_edges_around_sel_verts.append(ed_idx)
CoDEmanX
committed
# 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]] = []
CoDEmanX
committed
if not ed.vertices[1] in verts_connected_to_every_vert:
verts_connected_to_every_vert[ed.vertices[1]] = []
CoDEmanX
committed
verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
CoDEmanX
committed
all_verts_part_of_faces = []
all_edges_faces_count = []
all_edges_faces_count += self.edge_face_count(object)
CoDEmanX
committed
# 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]
CoDEmanX
committed
if all_edges_faces_count[ed_idx] > 0:
ed = object.data.edges[ed_idx]
CoDEmanX
committed
if not ed.vertices[0] in selected_verts_with_faces:
selected_verts_with_faces.append(ed.vertices[0])
CoDEmanX
committed
if not ed.vertices[1] in selected_verts_with_faces:
selected_verts_with_faces.append(ed.vertices[1])
CoDEmanX
committed
all_verts_part_of_faces.append(ed.vertices[0])
all_verts_part_of_faces.append(ed.vertices[1])
CoDEmanX
committed
CoDEmanX
committed
# Discard unneeded verts from calculations
participating_verts = []
movable_verts = []
for v_idx in all_selected_verts:
vert_has_edges_with_one_face = False
CoDEmanX
committed
# 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
CoDEmanX
committed
# 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)
CoDEmanX
committed
if v_idx not in all_verts_part_of_faces:
CoDEmanX
committed
# 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]
CoDEmanX
committed
for actual_v_idx in all_selected_verts:
count_shared_neighbors = 0
checked_verts = []
CoDEmanX
committed
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)
CoDEmanX
committed
if actual_v_idx in mv_connected_verts:
freeze_vert = True
break
CoDEmanX
committed
if count_shared_neighbors == 2:
freeze_vert = True
break
CoDEmanX
committed
CoDEmanX
committed
if freeze_vert:
movable_verts.remove(mv_idx)
CoDEmanX
committed
# 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]]
CoDEmanX
committed
CoDEmanX
committed
if shortest_edge_length is None:
shortest_edge_length = length
else:
if length < shortest_edge_length:
shortest_edge_length = length
CoDEmanX
committed
if shortest_edge_length is not None:
edges_merge_distance = shortest_edge_length * 0.5
else:
edges_merge_distance = 0
CoDEmanX
committed
# 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 = {}
CoDEmanX
committed
CoDEmanX
committed
v1_co = object.data.vertices[v1_idx].co
coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
CoDEmanX
committed
for v2_idx in remaining_verts:
if v1_idx != v2_idx:
v2_co = object.data.vertices[v2_idx].co
CoDEmanX
committed
CoDEmanX
committed
if dist <= edges_merge_distance: # Add the verts which are near enough
CoDEmanX
committed
coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
CoDEmanX
committed
for vm_idx in verts_to_merge:
remaining_verts.remove(vm_idx)
CoDEmanX
committed
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
CoDEmanX
committed
sum_x_co += v_co[0]
sum_y_co += v_co[1]
sum_z_co += v_co[2]
CoDEmanX
committed
movable_verts_to_merge_count += 1
CoDEmanX
committed
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
]
CoDEmanX
committed
# 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)
CoDEmanX
committed
# 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)
CoDEmanX
committed
shortest_dist = dist
nearest_vert_idx = vcf_idx
else:
if dist < shortest_dist:
shortest_dist = dist
nearest_vert_idx = vcf_idx
CoDEmanX
committed
coords = object.data.vertices[nearest_vert_idx].co
CoDEmanX
committed
target_point_co = [coords[0], coords[1], coords[2]]
CoDEmanX
committed
# 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]
CoDEmanX
committed
# 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)
CoDEmanX
committed
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
CoDEmanX
committed
# Get all the definitive selected edges, after weldding
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)
CoDEmanX
committed
# Save all the edges that belong to each vertex.
if not ed.vertices[0] in edges_per_vert:
edges_per_vert[ed.vertices[0]] = []
CoDEmanX
committed
if not ed.vertices[1] in edges_per_vert:
edges_per_vert[ed.vertices[1]] = []
CoDEmanX
committed
edges_per_vert[ed.vertices[0]].append(ed.index)
edges_per_vert[ed.vertices[1]].append(ed.index)
CoDEmanX
committed
# 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
CoDEmanX
committed
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
CoDEmanX
committed
selected_verts_idx = []
for v in object.data.vertices:
if v.select:
selected_verts_idx.append(v.index)
CoDEmanX
committed
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])
CoDEmanX
committed
if len(face.vertices) == 4:
face_verts.append(face.vertices[3])
CoDEmanX
committed
all_object_faces_verts_idx.append(face_verts)
CoDEmanX
committed
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')
CoDEmanX
committed
# 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]
CoDEmanX
committed
if not verts_surrounded_by_faces[ed.vertices[0]]:
if not ed.vertices[0] in related_key_verts:
related_key_verts[ed.vertices[0]] = []
CoDEmanX
committed
if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
related_key_verts[ed.vertices[0]].append(ed.vertices[1])
CoDEmanX
committed
if not verts_surrounded_by_faces[ed.vertices[1]]:
if not ed.vertices[1] in related_key_verts:
related_key_verts[ed.vertices[1]] = []
CoDEmanX
committed
if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
related_key_verts[ed.vertices[1]].append(ed.vertices[0])
CoDEmanX
committed
# Get groups of verts forming each face
CoDEmanX
committed
faces_verts_idx = []
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)
CoDEmanX
committed
if v2 in related_key_verts[v1]:
v2_in_rel_v1 = True
CoDEmanX
committed
if v1 in related_key_verts[v2]:
v1_in_rel_v2 = True
CoDEmanX
committed
# 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
CoDEmanX
committed
for f_verts in all_faces_to_check_idx:
repeated_verts = 0
CoDEmanX
committed
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
CoDEmanX
committed
if repeated_verts == len(f_verts):
repeated_face = True
break
CoDEmanX
committed
faces_verts_idx.append(
[v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
)
CoDEmanX
committed
# 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
CoDEmanX
committed
for f_verts in all_faces_to_check_idx:
repeated_verts = 0
CoDEmanX
committed
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
CoDEmanX
committed
if repeated_verts == len(f_verts):
repeated_face = True
break
CoDEmanX
committed
if not repeated_face:
faces_verts_idx.append([v1, related_verts_in_common[0], v2])
CoDEmanX
committed
# 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
CoDEmanX
committed
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)
CoDEmanX
committed
# Build faces discarding the ones in faces_to_not_include
me = object.data
bm = bmesh.new()
bm.from_mesh(me)
CoDEmanX
committed
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]])
CoDEmanX
committed
CoDEmanX
committed
CoDEmanX
committed
for v_idx in selected_verts_idx:
self.main_object.data.vertices[v_idx].select = True
CoDEmanX
committed
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')
Spivak Vladimir (cwolf3d)
committed
self.update()
CoDEmanX
committed
CoDEmanX
committed
def crosshatch_surface_invoke(self, ob_original_splines):
self.is_crosshatch = False
self.crosshatch_merge_distance = 0
CoDEmanX
committed
objects_to_delete = [] # duplicated strokes to be deleted.
CoDEmanX
committed
# 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
CoDEmanX
committed
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
CoDEmanX
committed
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"
CoDEmanX
committed
# 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]
CoDEmanX
committed
if coords_first_pt != sp.bezier_points[0].co:
first_dist = (coords_first_pt - sp.bezier_points[0].co).length
CoDEmanX
committed
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
CoDEmanX
committed
first_point_dist_sum += first_dist + second_dist
CoDEmanX
committed
if i == 0:
if first_dist != 0:
shortest_dist = first_dist
elif second_dist != 0:
shortest_dist = second_dist
CoDEmanX
committed
if shortest_dist > first_dist and first_dist != 0:
shortest_dist = first_dist
CoDEmanX
committed
if shortest_dist > second_dist and second_dist != 0:
shortest_dist = second_dist
CoDEmanX
committed
self.crosshatch_merge_distance = shortest_dist / 20
CoDEmanX
committed
CoDEmanX
committed
bpy.ops.object.duplicate('INVOKE_REGION_WIN')
CoDEmanX
committed
ob_calc_merge_dist = bpy.context.object
ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
CoDEmanX
committed
objects_to_delete.append(ob_calc_merge_dist)
CoDEmanX
committed
# 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')
CoDEmanX
committed
for i in range(4):
bpy.ops.curve.smooth('INVOKE_REGION_WIN')
CoDEmanX
committed
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
CoDEmanX
committed
ob_calc_merge_dist.data.resolution_u = 12
bpy.ops.object.convert(target='MESH', keep_original=False)
CoDEmanX
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=self.crosshatch_merge_distance)
bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
CoDEmanX
committed
# Remove verts with less than three edges
verts_edges_count = {}
for ed in ob_calc_merge_dist.data.edges:
v = ed.vertices
CoDEmanX
committed
if v[0] not in verts_edges_count:
verts_edges_count[v[0]] = 0
CoDEmanX
committed
if v[1] not in verts_edges_count:
verts_edges_count[v[1]] = 0
CoDEmanX
committed
verts_edges_count[v[0]] += 1
verts_edges_count[v[1]] += 1
CoDEmanX
committed
nodes_verts_coords = []
for v_idx in verts_edges_count:
v = ob_calc_merge_dist.data.vertices[v_idx]
CoDEmanX
committed
if verts_edges_count[v_idx] < 3:
v.select = True
CoDEmanX
committed
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')
CoDEmanX
committed
# 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')
CoDEmanX
committed
# 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]
CoDEmanX
committed
# Check if the strokes are a crosshatch
if len(nodes_verts_coords) >= 3:
self.is_crosshatch = True
CoDEmanX
committed
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
CoDEmanX
committed
if dist < shortest_dist:
shortest_dist = dist
else:
shortest_dist = dist
CoDEmanX
committed
self.crosshatch_merge_distance = shortest_dist / 3
CoDEmanX
committed
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.context.view_layer.objects.active = ob_splines
CoDEmanX
committed
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')
CoDEmanX
committed
# 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
CoDEmanX
committed
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]
CoDEmanX
committed
vec_1 = p1.co - p2.co
vec_2 = p2.co - p3.co
CoDEmanX
committed
if p2.co != p1.co and p2.co != p3.co:
angle = vec_1.angle(vec_2)
angle_sum += degrees(angle)
CoDEmanX
committed
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
CoDEmanX
committed
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
CoDEmanX
committed
CoDEmanX
committed
sp.bezier_points[0].select_control_point = False
sp.bezier_points[0].select_left_handle = False
sp.bezier_points[0].select_right_handle = False
CoDEmanX
committed
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
CoDEmanX
committed
# Smooth out strokes a little to improve crosshatch detection
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
CoDEmanX
committed
for i in range(15):
bpy.ops.curve.smooth('INVOKE_REGION_WIN')
CoDEmanX
committed
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
CoDEmanX
committed
for sp in ob_splines.data.splines:
angle_sum = 0
CoDEmanX
committed
sp.bezier_points[0].select_control_point = True
sp.bezier_points[0].select_left_handle = True
sp.bezier_points[0].select_right_handle = True
CoDEmanX
committed
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
CoDEmanX
committed
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]
CoDEmanX
committed
vec_1 = p1.co - p2.co
vec_2 = p2.co - p3.co
CoDEmanX
committed
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
CoDEmanX
committed
p2.select_control_point = True
p2.select_left_handle = True
p2.select_right_handle = True
CoDEmanX
committed
p3.select_control_point = True
p3.select_left_handle = True
p3.select_right_handle = True
CoDEmanX
committed
CoDEmanX
committed
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.select_all(action='INVERT')
CoDEmanX
committed
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
CoDEmanX
committed
objects_to_delete.append(ob_splines)
CoDEmanX
committed
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')
CoDEmanX
committed
# 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([])
CoDEmanX
committed
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]]
CoDEmanX
committed
all_intersections = []
checked_splines = []
for i in range(len(all_points_coords)):
CoDEmanX
committed
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]
CoDEmanX
committed
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]
CoDEmanX
committed
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
CoDEmanX
committed
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])
CoDEmanX
committed
checked_splines.append(i)
# 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))
CoDEmanX
committed
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]] = []
CoDEmanX
committed
self.crosshatch_strokes_coords[all_intersections[i][0]].append(
all_intersections[i][3]
) # Save intersection coords
else:
self.is_crosshatch = False
CoDEmanX
committed
bpy.ops.object.delete({"selected_objects": objects_to_delete})
CoDEmanX
committed
# 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]
CoDEmanX
committed
self.update()
CoDEmanX
committed
# 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)
CoDEmanX
committed
self.main_object.modifiers[m_idx].show_viewport = False
CoDEmanX
committed
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
CoDEmanX
committed
me_name = "SURFSKIO_STK_TMP"
me = bpy.data.meshes.new(me_name)
CoDEmanX
committed
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]
CoDEmanX
committed
CoDEmanX
committed
if co_idx > 0:
all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
CoDEmanX
committed
me.from_pydata(all_verts_coords, all_edges, [])
ob = object_utils.object_data_add(context, me)
Vladimir Spivak(cwolf3d)
committed
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)
CoDEmanX
committed
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.context.view_layer.objects.active = ob
CoDEmanX
committed
# 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
CoDEmanX
committed
if i != t and t not in checked_verts:
dist = (verts[i].co - verts[t].co).length
CoDEmanX
committed
if dist < shortest_dist:
shortest_dist = dist
nearest_vert = t
else:
shortest_dist = dist
nearest_vert = t
CoDEmanX
committed
middle_location = (verts[i].co + verts[nearest_vert].co) / 2
CoDEmanX
committed
verts[i].co = middle_location
verts[nearest_vert].co = middle_location
CoDEmanX
committed
checked_verts.append(i)
checked_verts.append(nearest_vert)
CoDEmanX
committed
# 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]]
CoDEmanX
committed
lengths_sum += (v1.co - v2.co).length
CoDEmanX
committed
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
CoDEmanX
committed
bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
threshold=average_edge_length / 15.0)
CoDEmanX
committed
final_points_ob = bpy.context.view_layer.objects.active
CoDEmanX
committed
# 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]] = []
CoDEmanX
committed
if not ed.vertices[1] in related_key_verts:
related_key_verts[ed.vertices[1]] = []
CoDEmanX
committed
if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
related_key_verts[ed.vertices[0]].append(ed.vertices[1])
CoDEmanX
committed
if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
related_key_verts[ed.vertices[1]].append(ed.vertices[0])
CoDEmanX
committed
# Get groups of verts forming each face
CoDEmanX
committed
faces_verts_idx = []
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)
CoDEmanX
committed
if v2 in related_key_verts[v1]:
v2_in_rel_v1 = True
CoDEmanX
committed
if v1 in related_key_verts[v2]:
v1_in_rel_v2 = True
CoDEmanX
committed
# 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
CoDEmanX
committed
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
CoDEmanX
committed
if repeated_verts == len(f_verts):
repeated_face = True
break
CoDEmanX
committed
faces_verts_idx.append([v1, related_verts_in_common[0],
v2, related_verts_in_common[1]])
CoDEmanX
committed
# 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
CoDEmanX
committed
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
CoDEmanX
committed
if repeated_verts == len(f_verts):
repeated_face = True
break
CoDEmanX
committed
if not repeated_face:
faces_verts_idx.append([v1, related_verts_in_common[0], v2])
CoDEmanX
committed
# 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
CoDEmanX
committed
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)
CoDEmanX
committed
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]])
CoDEmanX
committed
# 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)
CoDEmanX
committed
CoDEmanX
committed
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)
Vladimir Spivak(cwolf3d)
committed
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)
CoDEmanX
committed
# Delete final points temporal object
bpy.ops.object.delete({"selected_objects": [final_points_ob]})
CoDEmanX
committed
# Delete isolated verts if there are any
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.context.view_layer.objects.active = ob_surface
CoDEmanX
committed
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')
CoDEmanX
committed
# Join crosshatch results with original mesh
CoDEmanX
committed
# 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
CoDEmanX
committed
# 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]] = []
CoDEmanX
committed
surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
CoDEmanX
committed
if ed.vertices[1] not in surface_connected_verts:
surface_connected_verts[ed.vertices[1]] = []
CoDEmanX
committed
surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
CoDEmanX
committed
# 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')
CoDEmanX
committed
bpy.ops.object.duplicate('INVOKE_REGION_WIN')
CoDEmanX
committed
final_ob_duplicate = bpy.context.view_layer.objects.active
CoDEmanX
committed
shrinkwrap_modifier = context.object.modifiers.new("", 'SHRINKWRAP')
shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
shrinkwrap_modifier.target = self.main_object
CoDEmanX
committed
bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier=shrinkwrap_modifier.name)
CoDEmanX
committed
# 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
CoDEmanX
committed
# 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
CoDEmanX
committed
main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
CoDEmanX
committed
CoDEmanX
committed
# 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
Spivak Vladimir (cwolf3d)
committed
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)
CoDEmanX
committed
Spivak Vladimir (cwolf3d)
committed
points_target = []
points_target.append(ob_surface.data.vertices[c_v_idx].co)
points_target.append(final_ob_duplicate.data.vertices[i].co)
CoDEmanX
committed
Spivak Vladimir (cwolf3d)
committed
vec_A = points_original[0] - points_original[1]
vec_B = points_target[0] - points_target[1]
CoDEmanX
committed
Spivak Vladimir (cwolf3d)
committed
dist_A = (points_original[0] - points_original[1]).length
dist_B = (points_target[0] - points_target[1]).length
CoDEmanX
committed
Spivak Vladimir (cwolf3d)
committed
if not (
points_original[0] == points_original[1] or
points_target[0] == points_target[1]
): # If any vector's length is zero
CoDEmanX
committed
Spivak Vladimir (cwolf3d)
committed
angle = vec_A.angle(vec_B) / pi
else:
angle = 0
CoDEmanX
committed
Spivak Vladimir (cwolf3d)
committed
# 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:
CoDEmanX
committed
Spivak Vladimir (cwolf3d)
committed
merge_actual_vert = False
break
else:
merge_actual_vert = False
except:
self.report({'WARNING'},
"Crosshatch set incorrectly")
CoDEmanX
committed
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
CoDEmanX
committed
comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
CoDEmanX
committed
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:
CoDEmanX
committed
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)
CoDEmanX
committed
# 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
CoDEmanX
committed
bpy.ops.object.delete({"selected_objects": [final_ob_duplicate]})
CoDEmanX
committed
# Join crosshatched surface and main object
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.context.view_layer.objects.active = self.main_object
CoDEmanX
committed
bpy.ops.object.join('INVOKE_REGION_WIN')
CoDEmanX
committed
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)
CoDEmanX
committed
bpy.ops.mesh.select_all(action='DESELECT')
CoDEmanX
committed
# 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]
Spivak Vladimir (cwolf3d)
committed
self.update()
Spivak Vladimir (cwolf3d)
committed
CoDEmanX
committed
def rectangular_surface(self, context):
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)
CoDEmanX
committed
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]])
CoDEmanX
committed
# 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])
CoDEmanX
committed
# 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)
CoDEmanX
committed
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):
CoDEmanX
committed
edges_connected_to_tips.append(ed)
CoDEmanX
committed
# 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
CoDEmanX
committed
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:
CoDEmanX
committed
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])
CoDEmanX
committed
# 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
)
CoDEmanX
committed
verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
CoDEmanX
committed
checked_v.append(v_chain[0].index)
checked_v.append(v_chain[len(v_chain) - 1].index)
CoDEmanX
committed
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)
CoDEmanX
committed
# 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.")
CoDEmanX
committed
if len(all_chains_tips_idx) == 2: # If there are 2 tips
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
Loading
Loading full blame...