Update addons for changes to proportional edit mode
[blender-addons.git] / mesh_bsurfaces.py
blob4d31edac732d37f26001087546b3322f75e5ed30
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; version 2
6 # of the License.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
20 bl_info = {
21 "name": "Bsurfaces GPL Edition",
22 "author": "Eclectiel, Spivak Vladimir(cwolf3d)",
23 "version": (1, 5, 2),
24 "blender": (2, 80, 0),
25 "location": "View3D > EditMode > ToolShelf",
26 "description": "Modeling and retopology tool",
27 "wiki_url": "https://wiki.blender.org/index.php/Dev:Ref/Release_Notes/2.64/Bsurfaces_1.5",
28 "category": "Mesh",
32 import bpy
33 import bmesh
34 from bpy_extras import object_utils
36 import operator
37 from mathutils import Matrix, Vector
38 from mathutils.geometry import (
39 intersect_line_line,
40 intersect_point_line,
42 from math import (
43 degrees,
44 pi,
45 sqrt,
47 from bpy.props import (
48 BoolProperty,
49 FloatProperty,
50 IntProperty,
51 StringProperty,
52 PointerProperty,
54 from bpy.types import (
55 Operator,
56 Panel,
57 PropertyGroup,
58 AddonPreferences,
62 class VIEW3D_PT_tools_SURFSK_mesh(Panel):
63 bl_space_type = 'VIEW_3D'
64 bl_region_type = 'UI'
65 bl_category = 'Tools'
66 #bl_context = "mesh_edit"
67 bl_label = "Bsurfaces"
69 def draw(self, context):
70 layout = self.layout
71 scn = context.scene.bsurfaces
73 col = layout.column(align=True)
74 row = layout.row()
75 row.separator()
76 col.operator("gpencil.surfsk_init", text="Initialize")
77 col.prop(scn, "SURFSK_object_with_retopology")
78 col.prop(scn, "SURFSK_object_with_strokes")
79 col.separator()
80 col.operator("gpencil.surfsk_add_surface", text="Add Surface")
81 col.operator("gpencil.surfsk_add_strokes", text="Add Strokes")
82 col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
83 col.prop(scn, "SURFSK_cyclic_cross")
84 col.prop(scn, "SURFSK_cyclic_follow")
85 col.prop(scn, "SURFSK_loops_on_strokes")
86 col.prop(scn, "SURFSK_automatic_join")
87 col.prop(scn, "SURFSK_keep_strokes")
89 class VIEW3D_PT_tools_SURFSK_curve(Panel):
90 bl_space_type = 'VIEW_3D'
91 bl_region_type = 'UI'
92 bl_context = "curve_edit"
93 bl_category = 'Tools'
94 bl_label = "Bsurfaces"
96 @classmethod
97 def poll(cls, context):
98 return context.active_object
100 def draw(self, context):
101 layout = self.layout
103 col = layout.column(align=True)
104 row = layout.row()
105 row.separator()
106 col.operator("curve.surfsk_first_points", text="Set First Points")
107 col.operator("curve.switch_direction", text="Switch Direction")
108 col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
111 # Returns the type of strokes used
112 def get_strokes_type():
113 strokes_type = ""
114 strokes_num = 0
116 # Check if they are grease pencil
117 try:
118 gpencil = bpy.context.scene.bsurfaces.SURFSK_object_with_strokes
119 layer = gpencil.data.layers[0]
120 frame = layer.frames[0]
122 strokes_num = len(frame.strokes)
124 if strokes_num > 0:
125 strokes_type = "GP_STROKES"
126 except:
127 pass
129 # Check if they are mesh
130 try:
131 main_object = bpy.context.scene.bsurfaces.SURFSK_object_with_retopology
132 except:
133 pass
135 # Check if they are curves, if there aren't grease pencil strokes
136 if strokes_type == "":
137 if len(bpy.context.selected_objects) == 2:
138 for ob in bpy.context.selected_objects:
139 if ob != bpy.context.view_layer.objects.active and ob.type == "CURVE":
140 strokes_type = "EXTERNAL_CURVE"
141 strokes_num = len(ob.data.splines)
143 # Check if there is any non-bezier spline
144 for i in range(len(ob.data.splines)):
145 if ob.data.splines[i].type != "BEZIER":
146 strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
147 break
149 elif ob != bpy.context.view_layer.objects.active and ob.type != "CURVE":
150 strokes_type = "EXTERNAL_NO_CURVE"
151 elif len(bpy.context.selected_objects) > 2:
152 strokes_type = "MORE_THAN_ONE_EXTERNAL"
154 # Check if there is a single stroke without any selection in the object
155 if strokes_num == 1 and main_object.data.total_vert_sel == 0:
156 if strokes_type == "EXTERNAL_CURVE":
157 strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
158 elif strokes_type == "GP_STROKES":
159 strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"
161 if strokes_num == 0 and main_object.data.total_vert_sel > 0:
162 strokes_type = "SELECTION_ALONE"
164 if strokes_type == "":
165 strokes_type = "NO_STROKES"
167 return strokes_type
170 # Surface generator operator
171 class GPENCIL_OT_SURFSK_add_surface(Operator):
172 bl_idname = "gpencil.surfsk_add_surface"
173 bl_label = "Bsurfaces add surface"
174 bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
175 bl_options = {'REGISTER', 'UNDO'}
177 is_fill_faces: BoolProperty(
178 default=True
180 selection_U_exists: BoolProperty(
181 default=False
183 selection_V_exists: BoolProperty(
184 default=False
186 selection_U2_exists: BoolProperty(
187 default=False
189 selection_V2_exists: BoolProperty(
190 default=False
192 selection_V_is_closed: BoolProperty(
193 default=False
195 selection_U_is_closed: BoolProperty(
196 default=False
198 selection_V2_is_closed: BoolProperty(
199 default=False
201 selection_U2_is_closed: BoolProperty(
202 default=False
205 edges_U: IntProperty(
206 name="Cross",
207 description="Number of face-loops crossing the strokes",
208 default=1,
209 min=1,
210 max=200
212 edges_V: IntProperty(
213 name="Follow",
214 description="Number of face-loops following the strokes",
215 default=1,
216 min=1,
217 max=200
219 cyclic_cross: BoolProperty(
220 name="Cyclic Cross",
221 description="Make cyclic the face-loops crossing the strokes",
222 default=False
224 cyclic_follow: BoolProperty(
225 name="Cyclic Follow",
226 description="Make cyclic the face-loops following the strokes",
227 default=False
229 loops_on_strokes: BoolProperty(
230 name="Loops on strokes",
231 description="Make the loops match the paths of the strokes",
232 default=False
234 automatic_join: BoolProperty(
235 name="Automatic join",
236 description="Join automatically vertices of either surfaces generated "
237 "by crosshatching, or from the borders of closed shapes",
238 default=False
240 join_stretch_factor: FloatProperty(
241 name="Stretch",
242 description="Amount of stretching or shrinking allowed for "
243 "edges when joining vertices automatically",
244 default=1,
245 min=0,
246 max=3,
247 subtype='FACTOR'
249 keep_strokes: BoolProperty(
250 name="Keep strokes",
251 description="Keeps the sketched strokes or curves after adding the surface",
252 default=False
254 strokes_type: StringProperty()
255 initial_global_undo_state: BoolProperty()
258 def draw(self, context):
259 layout = self.layout
260 col = layout.column(align=True)
261 row = layout.row()
263 if not self.is_fill_faces:
264 row.separator()
265 if not self.is_crosshatch:
266 if not self.selection_U_exists:
267 col.prop(self, "edges_U")
268 row.separator()
270 if not self.selection_V_exists:
271 col.prop(self, "edges_V")
272 row.separator()
274 row.separator()
276 if not self.selection_U_exists:
277 if not (
278 (self.selection_V_exists and not self.selection_V_is_closed) or
279 (self.selection_V2_exists and not self.selection_V2_is_closed)
281 col.prop(self, "cyclic_cross")
283 if not self.selection_V_exists:
284 if not (
285 (self.selection_U_exists and not self.selection_U_is_closed) or
286 (self.selection_U2_exists and not self.selection_U2_is_closed)
288 col.prop(self, "cyclic_follow")
290 col.prop(self, "loops_on_strokes")
292 col.prop(self, "automatic_join")
294 if self.automatic_join:
295 row.separator()
296 col.separator()
297 row.separator()
298 col.prop(self, "join_stretch_factor")
300 col.prop(self, "keep_strokes")
302 # Get an ordered list of a chain of vertices
303 def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx,
304 first_vert_idx, middle_vertex_idx, closing_vert_idx):
305 # Order selected vertices.
306 verts_ordered = []
307 if closing_vert_idx is not None:
308 verts_ordered.append(ob.data.vertices[closing_vert_idx])
310 verts_ordered.append(ob.data.vertices[first_vert_idx])
311 prev_v = first_vert_idx
312 prev_ed = None
313 finish_while = False
314 while True:
315 edges_non_matched = 0
316 for i in all_selected_edges_idx:
317 if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and \
318 ob.data.edges[i].vertices[1] in all_selected_verts_idx:
320 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
321 prev_v = ob.data.edges[i].vertices[1]
322 prev_ed = ob.data.edges[i]
323 elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and \
324 ob.data.edges[i].vertices[0] in all_selected_verts_idx:
326 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
327 prev_v = ob.data.edges[i].vertices[0]
328 prev_ed = ob.data.edges[i]
329 else:
330 edges_non_matched += 1
332 if edges_non_matched == len(all_selected_edges_idx):
333 finish_while = True
335 if finish_while:
336 break
338 if closing_vert_idx is not None:
339 verts_ordered.append(ob.data.vertices[closing_vert_idx])
341 if middle_vertex_idx is not None:
342 verts_ordered.append(ob.data.vertices[middle_vertex_idx])
343 verts_ordered.reverse()
345 return tuple(verts_ordered)
347 # Calculates length of a chain of points.
348 def get_chain_length(self, object, verts_ordered):
349 matrix = object.matrix_world
351 edges_lengths = []
352 edges_lengths_sum = 0
353 for i in range(0, len(verts_ordered)):
354 if i == 0:
355 prev_v_co = matrix @ verts_ordered[i].co
356 else:
357 v_co = matrix @ verts_ordered[i].co
359 v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
360 edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
362 edges_lengths.append(edge_length)
363 edges_lengths_sum += edge_length
365 prev_v_co = v_co
367 return edges_lengths, edges_lengths_sum
369 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
370 def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
371 edges_proportions = []
372 if use_boundaries:
373 verts_count = 1
374 for l in edges_lengths:
375 edges_proportions.append(l / edges_lengths_sum)
376 verts_count += 1
377 else:
378 verts_count = 1
379 for n in range(0, fixed_edges_num):
380 edges_proportions.append(1 / fixed_edges_num)
381 verts_count += 1
383 return edges_proportions
385 # Calculates the angle between two pairs of points in space
386 def orientation_difference(self, points_A_co, points_B_co):
387 # each parameter should be a list with two elements,
388 # and each element should be a x,y,z coordinate
389 vec_A = points_A_co[0] - points_A_co[1]
390 vec_B = points_B_co[0] - points_B_co[1]
392 angle = vec_A.angle(vec_B)
394 if angle > 0.5 * pi:
395 angle = abs(angle - pi)
397 return angle
399 # Calculate the which vert of verts_idx list is the nearest one
400 # to the point_co coordinates, and the distance
401 def shortest_distance(self, object, point_co, verts_idx):
402 matrix = object.matrix_world
404 for i in range(0, len(verts_idx)):
405 dist = (point_co - matrix @ object.data.vertices[verts_idx[i]].co).length
406 if i == 0:
407 prev_dist = dist
408 nearest_vert_idx = verts_idx[i]
409 shortest_dist = dist
411 if dist < prev_dist:
412 prev_dist = dist
413 nearest_vert_idx = verts_idx[i]
414 shortest_dist = dist
416 return nearest_vert_idx, shortest_dist
418 # Returns the index of the opposite vert tip in a chain, given a vert tip index
419 # as parameter, and a multidimentional list with all pairs of tips
420 def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
421 opposite_vert_tip_idx = None
422 for i in range(0, len(all_chains_tips_idx)):
423 if vert_tip_idx == all_chains_tips_idx[i][0]:
424 opposite_vert_tip_idx = all_chains_tips_idx[i][1]
425 if vert_tip_idx == all_chains_tips_idx[i][1]:
426 opposite_vert_tip_idx = all_chains_tips_idx[i][0]
428 return opposite_vert_tip_idx
430 # Simplifies a spline and returns the new points coordinates
431 def simplify_spline(self, spline_coords, segments_num):
432 simplified_spline = []
433 points_between_segments = round(len(spline_coords) / segments_num)
435 simplified_spline.append(spline_coords[0])
436 for i in range(1, segments_num):
437 simplified_spline.append(spline_coords[i * points_between_segments])
439 simplified_spline.append(spline_coords[len(spline_coords) - 1])
441 return simplified_spline
443 # Cleans up the scene and gets it the same it was at the beginning,
444 # in case the script is interrupted in the middle of the execution
445 def cleanup_on_interruption(self):
446 # If the original strokes curve comes from conversion
447 # from grease pencil and wasn't made by hand, delete it
448 if not self.using_external_curves:
449 try:
450 bpy.ops.object.delete({"selected_objects": [self.original_curve]})
451 except:
452 pass
454 bpy.ops.object.delete({"selected_objects": [self.main_object]})
455 else:
456 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
457 self.original_curve.select_set(True)
458 self.main_object.select_set(True)
459 bpy.context.view_layer.objects.active = self.main_object
461 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
463 # Returns a list with the coords of the points distributed over the splines
464 # passed to this method according to the proportions parameter
465 def distribute_pts(self, surface_splines, proportions):
467 # Calculate the length of each final surface spline
468 surface_splines_lengths = []
469 surface_splines_parsed = []
471 for sp_idx in range(0, len(surface_splines)):
472 # Calculate spline length
473 surface_splines_lengths.append(0)
475 for i in range(0, len(surface_splines[sp_idx].bezier_points)):
476 if i == 0:
477 prev_p = surface_splines[sp_idx].bezier_points[i]
478 else:
479 p = surface_splines[sp_idx].bezier_points[i]
480 edge_length = (prev_p.co - p.co).length
481 surface_splines_lengths[sp_idx] += edge_length
483 prev_p = p
485 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
486 for sp_idx in range(0, len(surface_splines)):
487 surface_splines_parsed.append([])
488 surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
490 prev_p_co = surface_splines[sp_idx].bezier_points[0].co
491 p_idx = 0
493 for prop_idx in range(len(proportions) - 1):
494 target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
495 partial_segment_length = 0
496 finish_while = False
498 while True:
499 # if not it'll pass the p_idx as an index below and crash
500 if p_idx < len(surface_splines[sp_idx].bezier_points):
501 p_co = surface_splines[sp_idx].bezier_points[p_idx].co
502 new_dist = (prev_p_co - p_co).length
504 # The new distance that could have the partial segment if
505 # it is still shorter than the target length
506 potential_segment_length = partial_segment_length + new_dist
508 # If the potential is still shorter, keep adding
509 if potential_segment_length < target_length:
510 partial_segment_length = potential_segment_length
512 p_idx += 1
513 prev_p_co = p_co
515 # If the potential is longer than the target, calculate the target
516 # (a point between the last two points), and assign
517 elif potential_segment_length > target_length:
518 remaining_dist = target_length - partial_segment_length
519 vec = p_co - prev_p_co
520 vec.normalize()
521 intermediate_co = prev_p_co + (vec * remaining_dist)
523 surface_splines_parsed[sp_idx].append(intermediate_co)
525 partial_segment_length += remaining_dist
526 prev_p_co = intermediate_co
528 finish_while = True
530 # If the potential is equal to the target, assign
531 elif potential_segment_length == target_length:
532 surface_splines_parsed[sp_idx].append(p_co)
533 prev_p_co = p_co
535 finish_while = True
537 if finish_while:
538 break
540 # last point of the spline
541 surface_splines_parsed[sp_idx].append(
542 surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
545 return surface_splines_parsed
547 # Counts the number of faces that belong to each edge
548 def edge_face_count(self, ob):
549 ed_keys_count_dict = {}
551 for face in ob.data.polygons:
552 for ed_keys in face.edge_keys:
553 if ed_keys not in ed_keys_count_dict:
554 ed_keys_count_dict[ed_keys] = 1
555 else:
556 ed_keys_count_dict[ed_keys] += 1
558 edge_face_count = []
559 for i in range(len(ob.data.edges)):
560 edge_face_count.append(0)
562 for i in range(len(ob.data.edges)):
563 ed = ob.data.edges[i]
565 v1 = ed.vertices[0]
566 v2 = ed.vertices[1]
568 if (v1, v2) in ed_keys_count_dict:
569 edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
570 elif (v2, v1) in ed_keys_count_dict:
571 edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
573 return edge_face_count
575 # Fills with faces all the selected vertices which form empty triangles or quads
576 def fill_with_faces(self, object):
577 all_selected_verts_count = self.main_object_selected_verts_count
579 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
581 # Calculate average length of selected edges
582 all_selected_verts = []
583 original_sel_edges_count = 0
584 for ed in object.data.edges:
585 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
586 coords = []
587 coords.append(object.data.vertices[ed.vertices[0]].co)
588 coords.append(object.data.vertices[ed.vertices[1]].co)
590 original_sel_edges_count += 1
592 if not ed.vertices[0] in all_selected_verts:
593 all_selected_verts.append(ed.vertices[0])
595 if not ed.vertices[1] in all_selected_verts:
596 all_selected_verts.append(ed.vertices[1])
598 tuple(all_selected_verts)
600 # Check if there is any edge selected. If not, interrupt the script
601 if original_sel_edges_count == 0 and all_selected_verts_count > 0:
602 return 0
604 # Get all edges connected to selected verts
605 all_edges_around_sel_verts = []
606 edges_connected_to_sel_verts = {}
607 verts_connected_to_every_vert = {}
608 for ed_idx in range(len(object.data.edges)):
609 ed = object.data.edges[ed_idx]
610 include_edge = False
612 if ed.vertices[0] in all_selected_verts:
613 if not ed.vertices[0] in edges_connected_to_sel_verts:
614 edges_connected_to_sel_verts[ed.vertices[0]] = []
616 edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
617 include_edge = True
619 if ed.vertices[1] in all_selected_verts:
620 if not ed.vertices[1] in edges_connected_to_sel_verts:
621 edges_connected_to_sel_verts[ed.vertices[1]] = []
623 edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
624 include_edge = True
626 if include_edge is True:
627 all_edges_around_sel_verts.append(ed_idx)
629 # Get all connected verts to each vert
630 if not ed.vertices[0] in verts_connected_to_every_vert:
631 verts_connected_to_every_vert[ed.vertices[0]] = []
633 if not ed.vertices[1] in verts_connected_to_every_vert:
634 verts_connected_to_every_vert[ed.vertices[1]] = []
636 verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
637 verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
639 # Get all verts connected to faces
640 all_verts_part_of_faces = []
641 all_edges_faces_count = []
642 all_edges_faces_count += self.edge_face_count(object)
644 # Get only the selected edges that have faces attached.
645 count_faces_of_edges_around_sel_verts = {}
646 selected_verts_with_faces = []
647 for ed_idx in all_edges_around_sel_verts:
648 count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
650 if all_edges_faces_count[ed_idx] > 0:
651 ed = object.data.edges[ed_idx]
653 if not ed.vertices[0] in selected_verts_with_faces:
654 selected_verts_with_faces.append(ed.vertices[0])
656 if not ed.vertices[1] in selected_verts_with_faces:
657 selected_verts_with_faces.append(ed.vertices[1])
659 all_verts_part_of_faces.append(ed.vertices[0])
660 all_verts_part_of_faces.append(ed.vertices[1])
662 tuple(selected_verts_with_faces)
664 # Discard unneeded verts from calculations
665 participating_verts = []
666 movable_verts = []
667 for v_idx in all_selected_verts:
668 vert_has_edges_with_one_face = False
670 # Check if the actual vert has at least one edge connected to only one face
671 for ed_idx in edges_connected_to_sel_verts[v_idx]:
672 if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
673 vert_has_edges_with_one_face = True
675 # If the vert has two or less edges connected and the vert is not part of any face.
676 # Or the vert is part of any face and at least one of
677 # the connected edges has only one face attached to it.
678 if (len(edges_connected_to_sel_verts[v_idx]) == 2 and
679 v_idx not in all_verts_part_of_faces) or \
680 len(edges_connected_to_sel_verts[v_idx]) == 1 or \
681 (v_idx in all_verts_part_of_faces and
682 vert_has_edges_with_one_face):
684 participating_verts.append(v_idx)
686 if v_idx not in all_verts_part_of_faces:
687 movable_verts.append(v_idx)
689 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
690 for mv_idx in movable_verts:
691 freeze_vert = False
692 mv_connected_verts = verts_connected_to_every_vert[mv_idx]
694 for actual_v_idx in all_selected_verts:
695 count_shared_neighbors = 0
696 checked_verts = []
698 for mv_conn_v_idx in mv_connected_verts:
699 if mv_idx != actual_v_idx:
700 if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and \
701 mv_conn_v_idx not in checked_verts:
702 count_shared_neighbors += 1
703 checked_verts.append(mv_conn_v_idx)
705 if actual_v_idx in mv_connected_verts:
706 freeze_vert = True
707 break
709 if count_shared_neighbors == 2:
710 freeze_vert = True
711 break
713 if freeze_vert:
714 break
716 if freeze_vert:
717 movable_verts.remove(mv_idx)
719 # Calculate merge distance for participating verts
720 shortest_edge_length = None
721 for ed in object.data.edges:
722 if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
723 v1 = object.data.vertices[ed.vertices[0]]
724 v2 = object.data.vertices[ed.vertices[1]]
726 length = (v1.co - v2.co).length
728 if shortest_edge_length is None:
729 shortest_edge_length = length
730 else:
731 if length < shortest_edge_length:
732 shortest_edge_length = length
734 if shortest_edge_length is not None:
735 edges_merge_distance = shortest_edge_length * 0.5
736 else:
737 edges_merge_distance = 0
739 # Get together the verts near enough. They will be merged later
740 remaining_verts = []
741 remaining_verts += participating_verts
742 for v1_idx in participating_verts:
743 if v1_idx in remaining_verts and v1_idx in movable_verts:
744 verts_to_merge = []
745 coords_verts_to_merge = {}
747 verts_to_merge.append(v1_idx)
749 v1_co = object.data.vertices[v1_idx].co
750 coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
752 for v2_idx in remaining_verts:
753 if v1_idx != v2_idx:
754 v2_co = object.data.vertices[v2_idx].co
756 dist = (v1_co - v2_co).length
758 if dist <= edges_merge_distance: # Add the verts which are near enough
759 verts_to_merge.append(v2_idx)
761 coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
763 for vm_idx in verts_to_merge:
764 remaining_verts.remove(vm_idx)
766 if len(verts_to_merge) > 1:
767 # Calculate middle point of the verts to merge.
768 sum_x_co = 0
769 sum_y_co = 0
770 sum_z_co = 0
771 movable_verts_to_merge_count = 0
772 for i in range(len(verts_to_merge)):
773 if verts_to_merge[i] in movable_verts:
774 v_co = object.data.vertices[verts_to_merge[i]].co
776 sum_x_co += v_co[0]
777 sum_y_co += v_co[1]
778 sum_z_co += v_co[2]
780 movable_verts_to_merge_count += 1
782 middle_point_co = [
783 sum_x_co / movable_verts_to_merge_count,
784 sum_y_co / movable_verts_to_merge_count,
785 sum_z_co / movable_verts_to_merge_count
788 # Check if any vert to be merged is not movable
789 shortest_dist = None
790 are_verts_not_movable = False
791 verts_not_movable = []
792 for v_merge_idx in verts_to_merge:
793 if v_merge_idx in participating_verts and v_merge_idx not in movable_verts:
794 are_verts_not_movable = True
795 verts_not_movable.append(v_merge_idx)
797 if are_verts_not_movable:
798 # Get the vert connected to faces, that is nearest to
799 # the middle point of the movable verts
800 shortest_dist = None
801 for vcf_idx in verts_not_movable:
802 dist = abs((object.data.vertices[vcf_idx].co -
803 Vector(middle_point_co)).length)
805 if shortest_dist is None:
806 shortest_dist = dist
807 nearest_vert_idx = vcf_idx
808 else:
809 if dist < shortest_dist:
810 shortest_dist = dist
811 nearest_vert_idx = vcf_idx
813 coords = object.data.vertices[nearest_vert_idx].co
814 target_point_co = [coords[0], coords[1], coords[2]]
815 else:
816 target_point_co = middle_point_co
818 # Move verts to merge to the middle position
819 for v_merge_idx in verts_to_merge:
820 if v_merge_idx in movable_verts: # Only move the verts that are not part of faces
821 object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
822 object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
823 object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
825 # Perform "Remove Doubles" to weld all the disconnected verts
826 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
827 bpy.ops.mesh.remove_doubles(threshold=0.0001)
829 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
831 # Get all the definitive selected edges, after weldding
832 selected_edges = []
833 edges_per_vert = {} # Number of faces of each selected edge
834 for ed in object.data.edges:
835 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
836 selected_edges.append(ed.index)
838 # Save all the edges that belong to each vertex.
839 if not ed.vertices[0] in edges_per_vert:
840 edges_per_vert[ed.vertices[0]] = []
842 if not ed.vertices[1] in edges_per_vert:
843 edges_per_vert[ed.vertices[1]] = []
845 edges_per_vert[ed.vertices[0]].append(ed.index)
846 edges_per_vert[ed.vertices[1]].append(ed.index)
848 # Check if all the edges connected to each vert have two faces attached to them.
849 # To discard them later and make calculations faster
850 a = []
851 a += self.edge_face_count(object)
852 tuple(a)
853 verts_surrounded_by_faces = {}
854 for v_idx in edges_per_vert:
855 edges = edges_per_vert[v_idx]
856 edges_with_two_faces_count = 0
858 for ed_idx in edges_per_vert[v_idx]:
859 if a[ed_idx] == 2:
860 edges_with_two_faces_count += 1
862 if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
863 verts_surrounded_by_faces[v_idx] = True
864 else:
865 verts_surrounded_by_faces[v_idx] = False
867 # Get all the selected vertices
868 selected_verts_idx = []
869 for v in object.data.vertices:
870 if v.select:
871 selected_verts_idx.append(v.index)
873 # Get all the faces of the object
874 all_object_faces_verts_idx = []
875 for face in object.data.polygons:
876 face_verts = []
877 face_verts.append(face.vertices[0])
878 face_verts.append(face.vertices[1])
879 face_verts.append(face.vertices[2])
881 if len(face.vertices) == 4:
882 face_verts.append(face.vertices[3])
884 all_object_faces_verts_idx.append(face_verts)
886 # Deselect all vertices
887 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
888 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
889 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
891 # Make a dictionary with the verts related to each vert
892 related_key_verts = {}
893 for ed_idx in selected_edges:
894 ed = object.data.edges[ed_idx]
896 if not verts_surrounded_by_faces[ed.vertices[0]]:
897 if not ed.vertices[0] in related_key_verts:
898 related_key_verts[ed.vertices[0]] = []
900 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
901 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
903 if not verts_surrounded_by_faces[ed.vertices[1]]:
904 if not ed.vertices[1] in related_key_verts:
905 related_key_verts[ed.vertices[1]] = []
907 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
908 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
910 # Get groups of verts forming each face
911 faces_verts_idx = []
912 for v1 in related_key_verts: # verts-1 ....
913 for v2 in related_key_verts: # verts-2
914 if v1 != v2:
915 related_verts_in_common = []
916 v2_in_rel_v1 = False
917 v1_in_rel_v2 = False
918 for rel_v1 in related_key_verts[v1]:
919 # Check if related verts of verts-1 are related verts of verts-2
920 if rel_v1 in related_key_verts[v2]:
921 related_verts_in_common.append(rel_v1)
923 if v2 in related_key_verts[v1]:
924 v2_in_rel_v1 = True
926 if v1 in related_key_verts[v2]:
927 v1_in_rel_v2 = True
929 repeated_face = False
930 # If two verts have two related verts in common, they form a quad
931 if len(related_verts_in_common) == 2:
932 # Check if the face is already saved
933 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
935 for f_verts in all_faces_to_check_idx:
936 repeated_verts = 0
938 if len(f_verts) == 4:
939 if v1 in f_verts:
940 repeated_verts += 1
941 if v2 in f_verts:
942 repeated_verts += 1
943 if related_verts_in_common[0] in f_verts:
944 repeated_verts += 1
945 if related_verts_in_common[1] in f_verts:
946 repeated_verts += 1
948 if repeated_verts == len(f_verts):
949 repeated_face = True
950 break
952 if not repeated_face:
953 faces_verts_idx.append(
954 [v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
957 # If Two verts have one related vert in common and
958 # they are related to each other, they form a triangle
959 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
960 # Check if the face is already saved.
961 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
963 for f_verts in all_faces_to_check_idx:
964 repeated_verts = 0
966 if len(f_verts) == 3:
967 if v1 in f_verts:
968 repeated_verts += 1
969 if v2 in f_verts:
970 repeated_verts += 1
971 if related_verts_in_common[0] in f_verts:
972 repeated_verts += 1
974 if repeated_verts == len(f_verts):
975 repeated_face = True
976 break
978 if not repeated_face:
979 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
981 # Keep only the faces that don't overlap by ignoring quads
982 # that overlap with two adjacent triangles
983 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
984 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
985 for i in range(len(faces_verts_idx)):
986 for t in range(len(all_faces_to_check_idx)):
987 if i != t:
988 verts_in_common = 0
990 if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
991 for v_idx in all_faces_to_check_idx[t]:
992 if v_idx in faces_verts_idx[i]:
993 verts_in_common += 1
994 # If it doesn't have all it's vertices repeated in the other face
995 if verts_in_common == 3:
996 if i not in faces_to_not_include_idx:
997 faces_to_not_include_idx.append(i)
999 # Build faces discarding the ones in faces_to_not_include
1000 me = object.data
1001 bm = bmesh.new()
1002 bm.from_mesh(me)
1004 num_faces_created = 0
1005 for i in range(len(faces_verts_idx)):
1006 if i not in faces_to_not_include_idx:
1007 bm.faces.new([bm.verts[v] for v in faces_verts_idx[i]])
1009 num_faces_created += 1
1011 bm.to_mesh(me)
1012 bm.free()
1014 for v_idx in selected_verts_idx:
1015 self.main_object.data.vertices[v_idx].select = True
1017 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
1018 bpy.ops.mesh.normals_make_consistent(inside=False)
1019 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
1021 return num_faces_created
1023 # Crosshatch skinning
1024 def crosshatch_surface_invoke(self, ob_original_splines):
1025 self.is_crosshatch = False
1026 self.crosshatch_merge_distance = 0
1028 objects_to_delete = [] # duplicated strokes to be deleted.
1030 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1031 # (without this the surface verts merging with the main object doesn't work well)
1032 self.modifiers_prev_viewport_state = []
1033 if len(self.main_object.modifiers) > 0:
1034 for m_idx in range(len(self.main_object.modifiers)):
1035 self.modifiers_prev_viewport_state.append(
1036 self.main_object.modifiers[m_idx].show_viewport
1038 self.main_object.modifiers[m_idx].show_viewport = False
1040 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1041 ob_original_splines.select_set(True)
1042 bpy.context.view_layer.objects.active = ob_original_splines
1044 if len(ob_original_splines.data.splines) >= 2:
1045 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1046 ob_splines = bpy.context.object
1047 ob_splines.name = "SURFSKIO_NE_STR"
1049 # Get estimative merge distance (sum up the distances from the first point to
1050 # all other points, then average them and then divide them)
1051 first_point_dist_sum = 0
1052 first_dist = 0
1053 second_dist = 0
1054 coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
1055 for i in range(len(ob_splines.data.splines)):
1056 sp = ob_splines.data.splines[i]
1058 if coords_first_pt != sp.bezier_points[0].co:
1059 first_dist = (coords_first_pt - sp.bezier_points[0].co).length
1061 if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
1062 second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
1064 first_point_dist_sum += first_dist + second_dist
1066 if i == 0:
1067 if first_dist != 0:
1068 shortest_dist = first_dist
1069 elif second_dist != 0:
1070 shortest_dist = second_dist
1072 if shortest_dist > first_dist and first_dist != 0:
1073 shortest_dist = first_dist
1075 if shortest_dist > second_dist and second_dist != 0:
1076 shortest_dist = second_dist
1078 self.crosshatch_merge_distance = shortest_dist / 20
1080 # Recalculation of merge distance
1082 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1084 ob_calc_merge_dist = bpy.context.object
1085 ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
1087 objects_to_delete.append(ob_calc_merge_dist)
1089 # Smooth out strokes a little to improve crosshatch detection
1090 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1091 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
1093 for i in range(4):
1094 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1096 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1097 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1099 # Convert curves into mesh
1100 ob_calc_merge_dist.data.resolution_u = 12
1101 bpy.ops.object.convert(target='MESH', keep_original=False)
1103 # Find "intersection-nodes"
1104 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1105 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1106 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1107 threshold=self.crosshatch_merge_distance)
1108 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1109 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1111 # Remove verts with less than three edges
1112 verts_edges_count = {}
1113 for ed in ob_calc_merge_dist.data.edges:
1114 v = ed.vertices
1116 if v[0] not in verts_edges_count:
1117 verts_edges_count[v[0]] = 0
1119 if v[1] not in verts_edges_count:
1120 verts_edges_count[v[1]] = 0
1122 verts_edges_count[v[0]] += 1
1123 verts_edges_count[v[1]] += 1
1125 nodes_verts_coords = []
1126 for v_idx in verts_edges_count:
1127 v = ob_calc_merge_dist.data.vertices[v_idx]
1129 if verts_edges_count[v_idx] < 3:
1130 v.select = True
1132 # Remove them
1133 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1134 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
1135 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1137 # Remove doubles to discard very near verts from calculations of distance
1138 bpy.ops.mesh.remove_doubles(
1139 'INVOKE_REGION_WIN',
1140 threshold=self.crosshatch_merge_distance * 4.0
1142 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1144 # Get all coords of the resulting nodes
1145 nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for
1146 v in ob_calc_merge_dist.data.vertices]
1148 # Check if the strokes are a crosshatch
1149 if len(nodes_verts_coords) >= 3:
1150 self.is_crosshatch = True
1152 shortest_dist = None
1153 for co_1 in nodes_verts_coords:
1154 for co_2 in nodes_verts_coords:
1155 if co_1 != co_2:
1156 dist = (Vector(co_1) - Vector(co_2)).length
1158 if shortest_dist is not None:
1159 if dist < shortest_dist:
1160 shortest_dist = dist
1161 else:
1162 shortest_dist = dist
1164 self.crosshatch_merge_distance = shortest_dist / 3
1166 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1167 ob_splines.select_set(True)
1168 bpy.context.view_layer.objects.active = ob_splines
1170 # Deselect all points
1171 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1172 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1173 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1175 # Smooth splines in a localized way, to eliminate "saw-teeth"
1176 # like shapes when there are many points
1177 for sp in ob_splines.data.splines:
1178 angle_sum = 0
1180 angle_limit = 2 # Degrees
1181 for t in range(len(sp.bezier_points)):
1182 # Because on each iteration it checks the "next two points"
1183 # of the actual. This way it doesn't go out of range
1184 if t <= len(sp.bezier_points) - 3:
1185 p1 = sp.bezier_points[t]
1186 p2 = sp.bezier_points[t + 1]
1187 p3 = sp.bezier_points[t + 2]
1189 vec_1 = p1.co - p2.co
1190 vec_2 = p2.co - p3.co
1192 if p2.co != p1.co and p2.co != p3.co:
1193 angle = vec_1.angle(vec_2)
1194 angle_sum += degrees(angle)
1196 if angle_sum >= angle_limit: # If sum of angles is grater than the limit
1197 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1198 p1.select_control_point = True
1199 p1.select_left_handle = True
1200 p1.select_right_handle = True
1202 p2.select_control_point = True
1203 p2.select_left_handle = True
1204 p2.select_right_handle = True
1206 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1207 p3.select_control_point = True
1208 p3.select_left_handle = True
1209 p3.select_right_handle = True
1211 angle_sum = 0
1213 sp.bezier_points[0].select_control_point = False
1214 sp.bezier_points[0].select_left_handle = False
1215 sp.bezier_points[0].select_right_handle = False
1217 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
1218 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
1219 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
1221 # Smooth out strokes a little to improve crosshatch detection
1222 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1224 for i in range(15):
1225 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1227 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1228 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1230 # Simplify the splines
1231 for sp in ob_splines.data.splines:
1232 angle_sum = 0
1234 sp.bezier_points[0].select_control_point = True
1235 sp.bezier_points[0].select_left_handle = True
1236 sp.bezier_points[0].select_right_handle = True
1238 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
1239 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
1240 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
1242 angle_limit = 15 # Degrees
1243 for t in range(len(sp.bezier_points)):
1244 # Because on each iteration it checks the "next two points"
1245 # of the actual. This way it doesn't go out of range
1246 if t <= len(sp.bezier_points) - 3:
1247 p1 = sp.bezier_points[t]
1248 p2 = sp.bezier_points[t + 1]
1249 p3 = sp.bezier_points[t + 2]
1251 vec_1 = p1.co - p2.co
1252 vec_2 = p2.co - p3.co
1254 if p2.co != p1.co and p2.co != p3.co:
1255 angle = vec_1.angle(vec_2)
1256 angle_sum += degrees(angle)
1257 # If sum of angles is grater than the limit
1258 if angle_sum >= angle_limit:
1259 p1.select_control_point = True
1260 p1.select_left_handle = True
1261 p1.select_right_handle = True
1263 p2.select_control_point = True
1264 p2.select_left_handle = True
1265 p2.select_right_handle = True
1267 p3.select_control_point = True
1268 p3.select_left_handle = True
1269 p3.select_right_handle = True
1271 angle_sum = 0
1273 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1274 bpy.ops.curve.select_all(action='INVERT')
1276 bpy.ops.curve.delete(type='VERT')
1277 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1279 objects_to_delete.append(ob_splines)
1281 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1282 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1283 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1285 # Check if the strokes are a crosshatch
1286 if self.is_crosshatch:
1287 all_points_coords = []
1288 for i in range(len(ob_splines.data.splines)):
1289 all_points_coords.append([])
1291 all_points_coords[i] = [Vector((x, y, z)) for
1292 x, y, z in [bp.co for
1293 bp in ob_splines.data.splines[i].bezier_points]]
1295 all_intersections = []
1296 checked_splines = []
1297 for i in range(len(all_points_coords)):
1299 for t in range(len(all_points_coords[i]) - 1):
1300 bp1_co = all_points_coords[i][t]
1301 bp2_co = all_points_coords[i][t + 1]
1303 for i2 in range(len(all_points_coords)):
1304 if i != i2 and i2 not in checked_splines:
1305 for t2 in range(len(all_points_coords[i2]) - 1):
1306 bp3_co = all_points_coords[i2][t2]
1307 bp4_co = all_points_coords[i2][t2 + 1]
1309 intersec_coords = intersect_line_line(
1310 bp1_co, bp2_co, bp3_co, bp4_co
1312 if intersec_coords is not None:
1313 dist = (intersec_coords[0] - intersec_coords[1]).length
1315 if dist <= self.crosshatch_merge_distance * 1.5:
1316 temp_co, percent1 = intersect_point_line(
1317 intersec_coords[0], bp1_co, bp2_co
1319 if (percent1 >= -0.02 and percent1 <= 1.02):
1320 temp_co, percent2 = intersect_point_line(
1321 intersec_coords[1], bp3_co, bp4_co
1323 if (percent2 >= -0.02 and percent2 <= 1.02):
1324 # Format: spline index, first point index from
1325 # corresponding segment, percentage from first point of
1326 # actual segment, coords of intersection point
1327 all_intersections.append(
1328 (i, t, percent1,
1329 ob_splines.matrix_world @ intersec_coords[0])
1331 all_intersections.append(
1332 (i2, t2, percent2,
1333 ob_splines.matrix_world @ intersec_coords[1])
1336 checked_splines.append(i)
1337 # Sort list by spline, then by corresponding first point index of segment,
1338 # and then by percentage from first point of segment: elements 0 and 1 respectively
1339 all_intersections.sort(key=operator.itemgetter(0, 1, 2))
1341 self.crosshatch_strokes_coords = {}
1342 for i in range(len(all_intersections)):
1343 if not all_intersections[i][0] in self.crosshatch_strokes_coords:
1344 self.crosshatch_strokes_coords[all_intersections[i][0]] = []
1346 self.crosshatch_strokes_coords[all_intersections[i][0]].append(
1347 all_intersections[i][3]
1348 ) # Save intersection coords
1349 else:
1350 self.is_crosshatch = False
1352 # Delete all duplicates
1353 bpy.ops.object.delete({"selected_objects": objects_to_delete})
1355 # If the main object has modifiers, turn their "viewport view status" to
1356 # what it was before the forced deactivation above
1357 if len(self.main_object.modifiers) > 0:
1358 for m_idx in range(len(self.main_object.modifiers)):
1359 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1361 return
1363 # Part of the Crosshatch process that is repeated when the operator is tweaked
1364 def crosshatch_surface_execute(self):
1365 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1366 # (without this the surface verts merging with the main object doesn't work well)
1367 self.modifiers_prev_viewport_state = []
1368 if len(self.main_object.modifiers) > 0:
1369 for m_idx in range(len(self.main_object.modifiers)):
1370 self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
1372 self.main_object.modifiers[m_idx].show_viewport = False
1374 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1376 me_name = "SURFSKIO_STK_TMP"
1377 me = bpy.data.meshes.new(me_name)
1379 all_verts_coords = []
1380 all_edges = []
1381 for st_idx in self.crosshatch_strokes_coords:
1382 for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
1383 coords = self.crosshatch_strokes_coords[st_idx][co_idx]
1385 all_verts_coords.append(coords)
1387 if co_idx > 0:
1388 all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
1390 me.from_pydata(all_verts_coords, all_edges, [])
1392 me.update()
1394 ob = bpy.data.objects.new(me_name, me)
1395 ob.data = me
1396 bpy.context.collection.objects.link(ob)
1398 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1399 ob.select_set(True)
1400 bpy.context.view_layer.objects.active = ob
1402 # Get together each vert and its nearest, to the middle position
1403 verts = ob.data.vertices
1404 checked_verts = []
1405 for i in range(len(verts)):
1406 shortest_dist = None
1408 if i not in checked_verts:
1409 for t in range(len(verts)):
1410 if i != t and t not in checked_verts:
1411 dist = (verts[i].co - verts[t].co).length
1413 if shortest_dist is not None:
1414 if dist < shortest_dist:
1415 shortest_dist = dist
1416 nearest_vert = t
1417 else:
1418 shortest_dist = dist
1419 nearest_vert = t
1421 middle_location = (verts[i].co + verts[nearest_vert].co) / 2
1423 verts[i].co = middle_location
1424 verts[nearest_vert].co = middle_location
1426 checked_verts.append(i)
1427 checked_verts.append(nearest_vert)
1429 # Calculate average length between all the generated edges
1430 ob = bpy.context.object
1431 lengths_sum = 0
1432 for ed in ob.data.edges:
1433 v1 = ob.data.vertices[ed.vertices[0]]
1434 v2 = ob.data.vertices[ed.vertices[1]]
1436 lengths_sum += (v1.co - v2.co).length
1438 edges_count = len(ob.data.edges)
1439 # possible division by zero here
1440 average_edge_length = lengths_sum / edges_count if edges_count != 0 else 0.0001
1442 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1443 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1444 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1445 threshold=average_edge_length / 15.0)
1446 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1448 final_points_ob = bpy.context.view_layer.objects.active
1450 # Make a dictionary with the verts related to each vert
1451 related_key_verts = {}
1452 for ed in final_points_ob.data.edges:
1453 if not ed.vertices[0] in related_key_verts:
1454 related_key_verts[ed.vertices[0]] = []
1456 if not ed.vertices[1] in related_key_verts:
1457 related_key_verts[ed.vertices[1]] = []
1459 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
1460 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
1462 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
1463 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
1465 # Get groups of verts forming each face
1466 faces_verts_idx = []
1467 for v1 in related_key_verts: # verts-1 ....
1468 for v2 in related_key_verts: # verts-2
1469 if v1 != v2:
1470 related_verts_in_common = []
1471 v2_in_rel_v1 = False
1472 v1_in_rel_v2 = False
1473 for rel_v1 in related_key_verts[v1]:
1474 # Check if related verts of verts-1 are related verts of verts-2
1475 if rel_v1 in related_key_verts[v2]:
1476 related_verts_in_common.append(rel_v1)
1478 if v2 in related_key_verts[v1]:
1479 v2_in_rel_v1 = True
1481 if v1 in related_key_verts[v2]:
1482 v1_in_rel_v2 = True
1484 repeated_face = False
1485 # If two verts have two related verts in common, they form a quad
1486 if len(related_verts_in_common) == 2:
1487 # Check if the face is already saved
1488 for f_verts in faces_verts_idx:
1489 repeated_verts = 0
1491 if len(f_verts) == 4:
1492 if v1 in f_verts:
1493 repeated_verts += 1
1494 if v2 in f_verts:
1495 repeated_verts += 1
1496 if related_verts_in_common[0] in f_verts:
1497 repeated_verts += 1
1498 if related_verts_in_common[1] in f_verts:
1499 repeated_verts += 1
1501 if repeated_verts == len(f_verts):
1502 repeated_face = True
1503 break
1505 if not repeated_face:
1506 faces_verts_idx.append([v1, related_verts_in_common[0],
1507 v2, related_verts_in_common[1]])
1509 # If Two verts have one related vert in common and they are
1510 # related to each other, they form a triangle
1511 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1512 # Check if the face is already saved.
1513 for f_verts in faces_verts_idx:
1514 repeated_verts = 0
1516 if len(f_verts) == 3:
1517 if v1 in f_verts:
1518 repeated_verts += 1
1519 if v2 in f_verts:
1520 repeated_verts += 1
1521 if related_verts_in_common[0] in f_verts:
1522 repeated_verts += 1
1524 if repeated_verts == len(f_verts):
1525 repeated_face = True
1526 break
1528 if not repeated_face:
1529 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1531 # Keep only the faces that don't overlap by ignoring
1532 # quads that overlap with two adjacent triangles
1533 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1534 for i in range(len(faces_verts_idx)):
1535 for t in range(len(faces_verts_idx)):
1536 if i != t:
1537 verts_in_common = 0
1539 if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
1540 for v_idx in faces_verts_idx[t]:
1541 if v_idx in faces_verts_idx[i]:
1542 verts_in_common += 1
1543 # If it doesn't have all it's vertices repeated in the other face
1544 if verts_in_common == 3:
1545 if i not in faces_to_not_include_idx:
1546 faces_to_not_include_idx.append(i)
1548 # Build surface
1549 all_surface_verts_co = []
1550 verts_idx_translation = {}
1551 for i in range(len(final_points_ob.data.vertices)):
1552 coords = final_points_ob.data.vertices[i].co
1553 all_surface_verts_co.append([coords[0], coords[1], coords[2]])
1555 # Verts of each face.
1556 all_surface_faces = []
1557 for i in range(len(faces_verts_idx)):
1558 if i not in faces_to_not_include_idx:
1559 face = []
1560 for v_idx in faces_verts_idx[i]:
1561 face.append(v_idx)
1563 all_surface_faces.append(face)
1565 # Build the mesh
1566 surf_me_name = "SURFSKIO_surface"
1567 me_surf = bpy.data.meshes.new(surf_me_name)
1569 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
1571 me_surf.update()
1573 ob_surface = bpy.data.objects.new(surf_me_name, me_surf)
1574 bpy.context.collection.objects.link(ob_surface)
1576 # Delete final points temporal object
1577 bpy.ops.object.delete({"selected_objects": [final_points_ob]})
1579 # Delete isolated verts if there are any
1580 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1581 ob_surface.select_set(True)
1582 bpy.context.view_layer.objects.active = ob_surface
1584 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1585 bpy.ops.mesh.select_all(action='DESELECT')
1586 bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
1587 bpy.ops.mesh.delete()
1588 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1590 # Join crosshatch results with original mesh
1592 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1593 edges_length_sum = 0
1594 for ed in ob_surface.data.edges:
1595 edges_length_sum += (
1596 ob_surface.data.vertices[ed.vertices[0]].co -
1597 ob_surface.data.vertices[ed.vertices[1]].co
1598 ).length
1600 if len(ob_surface.data.edges) > 0:
1601 average_surface_edges_length = edges_length_sum / len(ob_surface.data.edges)
1602 else:
1603 average_surface_edges_length = 0.0001
1605 # Make dictionary with all the verts connected to each vert, on the new surface object.
1606 surface_connected_verts = {}
1607 for ed in ob_surface.data.edges:
1608 if not ed.vertices[0] in surface_connected_verts:
1609 surface_connected_verts[ed.vertices[0]] = []
1611 surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
1613 if ed.vertices[1] not in surface_connected_verts:
1614 surface_connected_verts[ed.vertices[1]] = []
1616 surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
1618 # Duplicate the new surface object, and use shrinkwrap to
1619 # calculate later the nearest verts to the main object
1620 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1621 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1622 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1624 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1626 final_ob_duplicate = bpy.context.view_layer.objects.active
1628 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1629 shrinkwrap_modifier = final_ob_duplicate.modifiers[-1]
1630 shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
1631 shrinkwrap_modifier.target = self.main_object
1633 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier=shrinkwrap_modifier.name)
1635 # Make list with verts of original mesh as index and coords as value
1636 main_object_verts_coords = []
1637 for v in self.main_object.data.vertices:
1638 coords = self.main_object.matrix_world @ v.co
1640 # To avoid problems when taking "-0.00" as a different value as "0.00"
1641 for c in range(len(coords)):
1642 if "%.3f" % coords[c] == "-0.00":
1643 coords[c] = 0
1645 main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
1647 tuple(main_object_verts_coords)
1649 # Determine which verts will be merged, snap them to the nearest verts
1650 # on the original verts, and get them selected
1651 crosshatch_verts_to_merge = []
1652 if self.automatic_join:
1653 for i in range(len(ob_surface.data.vertices)):
1654 # Calculate the distance from each of the connected verts to the actual vert,
1655 # and compare it with the distance they would have if joined.
1656 # If they don't change much, that vert can be joined
1657 merge_actual_vert = True
1658 if len(surface_connected_verts[i]) < 4:
1659 for c_v_idx in surface_connected_verts[i]:
1660 points_original = []
1661 points_original.append(ob_surface.data.vertices[c_v_idx].co)
1662 points_original.append(ob_surface.data.vertices[i].co)
1664 points_target = []
1665 points_target.append(ob_surface.data.vertices[c_v_idx].co)
1666 points_target.append(final_ob_duplicate.data.vertices[i].co)
1668 vec_A = points_original[0] - points_original[1]
1669 vec_B = points_target[0] - points_target[1]
1671 dist_A = (points_original[0] - points_original[1]).length
1672 dist_B = (points_target[0] - points_target[1]).length
1674 if not (
1675 points_original[0] == points_original[1] or
1676 points_target[0] == points_target[1]
1677 ): # If any vector's length is zero
1679 angle = vec_A.angle(vec_B) / pi
1680 else:
1681 angle = 0
1683 # Set a range of acceptable variation in the connected edges
1684 if dist_B > dist_A * 1.7 * self.join_stretch_factor or \
1685 dist_B < dist_A / 2 / self.join_stretch_factor or \
1686 angle >= 0.15 * self.join_stretch_factor:
1688 merge_actual_vert = False
1689 break
1690 else:
1691 merge_actual_vert = False
1693 if merge_actual_vert:
1694 coords = final_ob_duplicate.data.vertices[i].co
1695 # To avoid problems when taking "-0.000" as a different value as "0.00"
1696 for c in range(len(coords)):
1697 if "%.3f" % coords[c] == "-0.00":
1698 coords[c] = 0
1700 comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
1702 if comparison_coords in main_object_verts_coords:
1703 # Get the index of the vert with those coords in the main object
1704 main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords)
1706 if self.main_object.data.vertices[main_object_related_vert_idx].select is True or \
1707 self.main_object_selected_verts_count == 0:
1709 ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
1710 ob_surface.data.vertices[i].select_set(True)
1711 crosshatch_verts_to_merge.append(i)
1713 # Make sure the vert in the main object is selected,
1714 # in case it wasn't selected and the "join crosshatch" option is active
1715 self.main_object.data.vertices[main_object_related_vert_idx].select_set(True)
1717 # Delete duplicated object
1718 bpy.ops.object.delete({"selected_objects": [final_ob_duplicate]})
1720 # Join crosshatched surface and main object
1721 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1722 ob_surface.select_set(True)
1723 self.main_object.select_set(True)
1724 bpy.context.view_layer.objects.active = self.main_object
1726 bpy.ops.object.join('INVOKE_REGION_WIN')
1728 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1729 # Perform Remove doubles to merge verts
1730 if not (self.automatic_join is False and self.main_object_selected_verts_count == 0):
1731 bpy.ops.mesh.remove_doubles(threshold=0.0001)
1733 bpy.ops.mesh.select_all(action='DESELECT')
1735 # If the main object has modifiers, turn their "viewport view status"
1736 # to what it was before the forced deactivation above
1737 if len(self.main_object.modifiers) > 0:
1738 for m_idx in range(len(self.main_object.modifiers)):
1739 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1741 return {'FINISHED'}
1743 def rectangular_surface(self):
1744 # Selected edges
1745 all_selected_edges_idx = []
1746 all_selected_verts = []
1747 all_verts_idx = []
1748 for ed in self.main_object.data.edges:
1749 if ed.select:
1750 all_selected_edges_idx.append(ed.index)
1752 # Selected vertices
1753 if not ed.vertices[0] in all_selected_verts:
1754 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
1755 if not ed.vertices[1] in all_selected_verts:
1756 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
1758 # All verts (both from each edge) to determine later
1759 # which are at the tips (those not repeated twice)
1760 all_verts_idx.append(ed.vertices[0])
1761 all_verts_idx.append(ed.vertices[1])
1763 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1764 all_chains_tips_idx = []
1765 for v_idx in all_verts_idx:
1766 if all_verts_idx.count(v_idx) < 2:
1767 all_chains_tips_idx.append(v_idx)
1769 edges_connected_to_tips = []
1770 for ed in self.main_object.data.edges:
1771 if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and \
1772 not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
1774 edges_connected_to_tips.append(ed)
1776 # Check closed selections
1777 # List with groups of three verts, where the first element of the pair is
1778 # the unselected vert of a closed selection and the other two elements are the
1779 # selected neighbor verts (it will be useful to determine which selection chain
1780 # the unselected vert belongs to, and determine the "middle-vertex")
1781 single_unselected_verts_and_neighbors = []
1783 # To identify a "closed" selection (a selection that is a closed chain except
1784 # for one vertex) find the vertex in common that have the edges connected to tips.
1785 # If there is a vertex in common, that one is the unselected vert that closes
1786 # the selection or is a "middle-vertex"
1787 single_unselected_verts = []
1788 for ed in edges_connected_to_tips:
1789 for ed_b in edges_connected_to_tips:
1790 if ed != ed_b:
1791 if ed.vertices[0] == ed_b.vertices[0] and \
1792 not self.main_object.data.vertices[ed.vertices[0]].select and \
1793 ed.vertices[0] not in single_unselected_verts:
1795 # The second element is one of the tips of the selected
1796 # vertices of the closed selection
1797 single_unselected_verts_and_neighbors.append(
1798 [ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]
1800 single_unselected_verts.append(ed.vertices[0])
1801 break
1802 elif ed.vertices[0] == ed_b.vertices[1] and \
1803 not self.main_object.data.vertices[ed.vertices[0]].select and \
1804 ed.vertices[0] not in single_unselected_verts:
1806 single_unselected_verts_and_neighbors.append(
1807 [ed.vertices[0], ed.vertices[1], ed_b.vertices[0]]
1809 single_unselected_verts.append(ed.vertices[0])
1810 break
1811 elif ed.vertices[1] == ed_b.vertices[0] and \
1812 not self.main_object.data.vertices[ed.vertices[1]].select and \
1813 ed.vertices[1] not in single_unselected_verts:
1815 single_unselected_verts_and_neighbors.append(
1816 [ed.vertices[1], ed.vertices[0], ed_b.vertices[1]]
1818 single_unselected_verts.append(ed.vertices[1])
1819 break
1820 elif ed.vertices[1] == ed_b.vertices[1] and \
1821 not self.main_object.data.vertices[ed.vertices[1]].select and \
1822 ed.vertices[1] not in single_unselected_verts:
1824 single_unselected_verts_and_neighbors.append(
1825 [ed.vertices[1], ed.vertices[0], ed_b.vertices[0]]
1827 single_unselected_verts.append(ed.vertices[1])
1828 break
1830 middle_vertex_idx = None
1831 tips_to_discard_idx = []
1833 # Check if there is a "middle-vertex", and get its index
1834 for i in range(0, len(single_unselected_verts_and_neighbors)):
1835 actual_chain_verts = self.get_ordered_verts(
1836 self.main_object, all_selected_edges_idx,
1837 all_verts_idx, single_unselected_verts_and_neighbors[i][1],
1838 None, None
1841 if single_unselected_verts_and_neighbors[i][2] != \
1842 actual_chain_verts[len(actual_chain_verts) - 1].index:
1844 middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
1845 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
1846 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
1848 # List with pairs of verts that belong to the tips of each selection chain (row)
1849 verts_tips_same_chain_idx = []
1850 if len(all_chains_tips_idx) >= 2:
1851 checked_v = []
1852 for i in range(0, len(all_chains_tips_idx)):
1853 if all_chains_tips_idx[i] not in checked_v:
1854 v_chain = self.get_ordered_verts(
1855 self.main_object, all_selected_edges_idx,
1856 all_verts_idx, all_chains_tips_idx[i],
1857 middle_vertex_idx, None
1860 verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
1862 checked_v.append(v_chain[0].index)
1863 checked_v.append(v_chain[len(v_chain) - 1].index)
1865 # Selection tips (vertices).
1866 verts_tips_parsed_idx = []
1867 if len(all_chains_tips_idx) >= 2:
1868 for spec_v_idx in all_chains_tips_idx:
1869 if (spec_v_idx not in tips_to_discard_idx):
1870 verts_tips_parsed_idx.append(spec_v_idx)
1872 # Identify the type of selection made by the user
1873 if middle_vertex_idx is not None:
1874 # If there are 4 tips (two selection chains), and
1875 # there is only one single unselected vert (the middle vert)
1876 if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1:
1877 selection_type = "TWO_CONNECTED"
1878 else:
1879 # The type of the selection was not identified, the script stops.
1880 self.report({'WARNING'}, "The selection isn't valid.")
1881 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1882 self.cleanup_on_interruption()
1883 self.stopping_errors = True
1885 return{'CANCELLED'}
1886 else:
1887 if len(all_chains_tips_idx) == 2: # If there are 2 tips
1888 selection_type = "SINGLE"
1889 elif len(all_chains_tips_idx) == 4: # If there are 4 tips
1890 selection_type = "TWO_NOT_CONNECTED"
1891 elif len(all_chains_tips_idx) == 0:
1892 if len(self.main_splines.data.splines) > 1:
1893 selection_type = "NO_SELECTION"
1894 else:
1895 # If the selection was not identified and there is only one stroke,
1896 # there's no possibility to build a surface, so the script is interrupted
1897 self.report({'WARNING'}, "The selection isn't valid.")
1898 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1899 self.cleanup_on_interruption()
1900 self.stopping_errors = True
1902 return{'CANCELLED'}
1903 else:
1904 # The type of the selection was not identified, the script stops
1905 self.report({'WARNING'}, "The selection isn't valid.")
1907 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1908 self.cleanup_on_interruption()
1910 self.stopping_errors = True
1912 return{'CANCELLED'}
1914 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1915 if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
1916 self.report({'WARNING'},
1917 "At least two strokes are needed when there are two not connected selections")
1918 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1919 self.cleanup_on_interruption()
1920 self.stopping_errors = True
1922 return{'CANCELLED'}
1924 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1926 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1927 self.main_splines.select_set(True)
1928 bpy.context.view_layer.objects.active = self.main_splines
1930 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1931 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1932 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1933 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1934 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1935 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1936 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1937 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1938 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1940 self.selection_U_exists = False
1941 self.selection_U2_exists = False
1942 self.selection_V_exists = False
1943 self.selection_V2_exists = False
1945 self.selection_U_is_closed = False
1946 self.selection_U2_is_closed = False
1947 self.selection_V_is_closed = False
1948 self.selection_V2_is_closed = False
1950 # Define what vertices are at the tips of each selection and are not the middle-vertex
1951 if selection_type == "TWO_CONNECTED":
1952 self.selection_U_exists = True
1953 self.selection_V_exists = True
1955 closing_vert_U_idx = None
1956 closing_vert_V_idx = None
1957 closing_vert_U2_idx = None
1958 closing_vert_V2_idx = None
1960 # Determine which selection is Selection-U and which is Selection-V
1961 points_A = []
1962 points_B = []
1963 points_first_stroke_tips = []
1965 points_A.append(
1966 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[0]].co
1968 points_A.append(
1969 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
1971 points_B.append(
1972 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[1]].co
1974 points_B.append(
1975 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
1977 points_first_stroke_tips.append(
1978 self.main_splines.data.splines[0].bezier_points[0].co
1980 points_first_stroke_tips.append(
1981 self.main_splines.data.splines[0].bezier_points[
1982 len(self.main_splines.data.splines[0].bezier_points) - 1
1983 ].co
1986 angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
1987 angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
1989 if angle_A < angle_B:
1990 first_vert_U_idx = verts_tips_parsed_idx[0]
1991 first_vert_V_idx = verts_tips_parsed_idx[1]
1992 else:
1993 first_vert_U_idx = verts_tips_parsed_idx[1]
1994 first_vert_V_idx = verts_tips_parsed_idx[0]
1996 elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
1997 first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
1998 last_sketched_point_first_stroke_co = \
1999 self.main_splines.data.splines[0].bezier_points[
2000 len(self.main_splines.data.splines[0].bezier_points) - 1
2001 ].co
2002 first_sketched_point_last_stroke_co = \
2003 self.main_splines.data.splines[
2004 len(self.main_splines.data.splines) - 1
2005 ].bezier_points[0].co
2006 if len(self.main_splines.data.splines) > 1:
2007 first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
2008 last_sketched_point_second_stroke_co = \
2009 self.main_splines.data.splines[1].bezier_points[
2010 len(self.main_splines.data.splines[1].bezier_points) - 1
2011 ].co
2013 single_unselected_neighbors = [] # Only the neighbors of the single unselected verts
2014 for verts_neig_idx in single_unselected_verts_and_neighbors:
2015 single_unselected_neighbors.append(verts_neig_idx[1])
2016 single_unselected_neighbors.append(verts_neig_idx[2])
2018 all_chains_tips_and_middle_vert = []
2019 for v_idx in all_chains_tips_idx:
2020 if v_idx not in single_unselected_neighbors:
2021 all_chains_tips_and_middle_vert.append(v_idx)
2023 all_chains_tips_and_middle_vert += single_unselected_verts
2025 all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
2027 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2028 nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = \
2029 self.shortest_distance(
2030 self.main_object,
2031 first_sketched_point_first_stroke_co,
2032 all_chains_tips_and_middle_vert
2034 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2035 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2036 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2038 nearest_tip_to_first_st_first_pt_opposite_idx = \
2039 self.opposite_tip(
2040 nearest_tip_to_first_st_first_pt_idx,
2041 verts_tips_same_chain_idx
2043 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2044 nearest_tip_to_first_st_last_pt_idx, temp_dist = \
2045 self.shortest_distance(
2046 self.main_object,
2047 last_sketched_point_first_stroke_co,
2048 all_chains_tips_and_middle_vert
2050 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2051 nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = \
2052 self.shortest_distance(
2053 self.main_object,
2054 first_sketched_point_last_stroke_co,
2055 all_chains_tips_and_middle_vert
2057 if len(self.main_splines.data.splines) > 1:
2058 # The selected vertex nearest to the first point of the second sketched stroke
2059 # (This will be useful to determine the direction of the closed
2060 # selection V when extruding along strokes)
2061 nearest_vert_to_second_st_first_pt_idx, temp_dist = \
2062 self.shortest_distance(
2063 self.main_object,
2064 first_sketched_point_second_stroke_co,
2065 all_verts_idx
2067 # The selected vertex nearest to the first point of the second sketched stroke
2068 # (This will be useful to determine the direction of the closed
2069 # selection V2 when extruding along strokes)
2070 nearest_vert_to_second_st_last_pt_idx, temp_dist = \
2071 self.shortest_distance(
2072 self.main_object,
2073 last_sketched_point_second_stroke_co,
2074 all_verts_idx
2076 # Determine if the single selection will be treated as U or as V
2077 edges_sum = 0
2078 for i in all_selected_edges_idx:
2079 edges_sum += (
2080 (self.main_object.matrix_world @
2081 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) -
2082 (self.main_object.matrix_world @
2083 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)
2084 ).length
2086 average_edge_length = edges_sum / len(all_selected_edges_idx)
2088 # Get shortest distance from the first point of the last stroke to any participating vertex
2089 temp_idx, shortest_distance_to_last_stroke = \
2090 self.shortest_distance(
2091 self.main_object,
2092 first_sketched_point_last_stroke_co,
2093 all_participating_verts
2095 # If the beginning of the first stroke is near enough, and its orientation
2096 # difference with the first edge of the nearest selection chain is not too high,
2097 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2098 if shortest_distance_to_first_stroke < average_edge_length / 4 and \
2099 shortest_distance_to_last_stroke < average_edge_length and \
2100 len(self.main_splines.data.splines) > 1:
2102 self.selection_U_exists = False
2103 self.selection_V_exists = True
2104 # If the first selection is not closed
2105 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2106 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2107 self.selection_V_is_closed = False
2108 first_neighbor_V_idx = None
2109 closing_vert_U_idx = None
2110 closing_vert_U2_idx = None
2111 closing_vert_V_idx = None
2112 closing_vert_V2_idx = None
2114 first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2116 if selection_type == "TWO_NOT_CONNECTED":
2117 self.selection_V2_exists = True
2119 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2120 else:
2121 self.selection_V_is_closed = True
2122 closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2124 # Get the neighbors of the first (unselected) vert of the closed selection U.
2125 vert_neighbors = []
2126 for verts in single_unselected_verts_and_neighbors:
2127 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2128 vert_neighbors.append(verts[1])
2129 vert_neighbors.append(verts[2])
2130 break
2132 verts_V = self.get_ordered_verts(
2133 self.main_object, all_selected_edges_idx,
2134 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2137 for i in range(0, len(verts_V)):
2138 if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
2139 # If the vertex nearest to the first point of the second stroke
2140 # is in the first half of the selected verts
2141 if i >= len(verts_V) / 2:
2142 first_vert_V_idx = vert_neighbors[1]
2143 break
2144 else:
2145 first_vert_V_idx = vert_neighbors[0]
2146 break
2148 if selection_type == "TWO_NOT_CONNECTED":
2149 self.selection_V2_exists = True
2150 # If the second selection is not closed
2151 if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or \
2152 nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx:
2154 self.selection_V2_is_closed = False
2155 first_neighbor_V2_idx = None
2156 closing_vert_V2_idx = None
2157 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2159 else:
2160 self.selection_V2_is_closed = True
2161 closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2163 # Get the neighbors of the first (unselected) vert of the closed selection U
2164 vert_neighbors = []
2165 for verts in single_unselected_verts_and_neighbors:
2166 if verts[0] == nearest_tip_to_first_st_last_pt_idx:
2167 vert_neighbors.append(verts[1])
2168 vert_neighbors.append(verts[2])
2169 break
2171 verts_V2 = self.get_ordered_verts(
2172 self.main_object, all_selected_edges_idx,
2173 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2176 for i in range(0, len(verts_V2)):
2177 if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
2178 # If the vertex nearest to the first point of the second stroke
2179 # is in the first half of the selected verts
2180 if i >= len(verts_V2) / 2:
2181 first_vert_V2_idx = vert_neighbors[1]
2182 break
2183 else:
2184 first_vert_V2_idx = vert_neighbors[0]
2185 break
2186 else:
2187 self.selection_V2_exists = False
2189 else:
2190 self.selection_U_exists = True
2191 self.selection_V_exists = False
2192 # If the first selection is not closed
2193 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2194 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2195 self.selection_U_is_closed = False
2196 first_neighbor_U_idx = None
2197 closing_vert_U_idx = None
2199 points_tips = []
2200 points_tips.append(
2201 self.main_object.matrix_world @
2202 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2204 points_tips.append(
2205 self.main_object.matrix_world @
2206 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co
2208 points_first_stroke_tips = []
2209 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2210 points_first_stroke_tips.append(
2211 self.main_splines.data.splines[0].bezier_points[
2212 len(self.main_splines.data.splines[0].bezier_points) - 1
2213 ].co
2215 vec_A = points_tips[0] - points_tips[1]
2216 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2218 # Compare the direction of the selection and the first
2219 # grease pencil stroke to determine which is the "first" vertex of the selection
2220 if vec_A.dot(vec_B) < 0:
2221 first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
2222 else:
2223 first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2225 else:
2226 self.selection_U_is_closed = True
2227 closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2229 # Get the neighbors of the first (unselected) vert of the closed selection U
2230 vert_neighbors = []
2231 for verts in single_unselected_verts_and_neighbors:
2232 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2233 vert_neighbors.append(verts[1])
2234 vert_neighbors.append(verts[2])
2235 break
2237 points_first_and_neighbor = []
2238 points_first_and_neighbor.append(
2239 self.main_object.matrix_world @
2240 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2242 points_first_and_neighbor.append(
2243 self.main_object.matrix_world @
2244 self.main_object.data.vertices[vert_neighbors[0]].co
2246 points_first_stroke_tips = []
2247 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2248 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
2250 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2251 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2253 # Compare the direction of the selection and the first grease pencil stroke to
2254 # determine which is the vertex neighbor to the first vertex (unselected) of
2255 # the closed selection. This will determine the direction of the closed selection
2256 if vec_A.dot(vec_B) < 0:
2257 first_vert_U_idx = vert_neighbors[1]
2258 else:
2259 first_vert_U_idx = vert_neighbors[0]
2261 if selection_type == "TWO_NOT_CONNECTED":
2262 self.selection_U2_exists = True
2263 # If the second selection is not closed
2264 if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or \
2265 nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx:
2267 self.selection_U2_is_closed = False
2268 first_neighbor_U2_idx = None
2269 closing_vert_U2_idx = None
2270 first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2271 else:
2272 self.selection_U2_is_closed = True
2273 closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2275 # Get the neighbors of the first (unselected) vert of the closed selection U
2276 vert_neighbors = []
2277 for verts in single_unselected_verts_and_neighbors:
2278 if verts[0] == nearest_tip_to_last_st_first_pt_idx:
2279 vert_neighbors.append(verts[1])
2280 vert_neighbors.append(verts[2])
2281 break
2283 points_first_and_neighbor = []
2284 points_first_and_neighbor.append(
2285 self.main_object.matrix_world @
2286 self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co
2288 points_first_and_neighbor.append(
2289 self.main_object.matrix_world @
2290 self.main_object.data.vertices[vert_neighbors[0]].co
2292 points_last_stroke_tips = []
2293 points_last_stroke_tips.append(
2294 self.main_splines.data.splines[
2295 len(self.main_splines.data.splines) - 1
2296 ].bezier_points[0].co
2298 points_last_stroke_tips.append(
2299 self.main_splines.data.splines[
2300 len(self.main_splines.data.splines) - 1
2301 ].bezier_points[1].co
2303 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2304 vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
2306 # Compare the direction of the selection and the last grease pencil stroke to
2307 # determine which is the vertex neighbor to the first vertex (unselected) of
2308 # the closed selection. This will determine the direction of the closed selection
2309 if vec_A.dot(vec_B) < 0:
2310 first_vert_U2_idx = vert_neighbors[1]
2311 else:
2312 first_vert_U2_idx = vert_neighbors[0]
2313 else:
2314 self.selection_U2_exists = False
2316 elif selection_type == "NO_SELECTION":
2317 self.selection_U_exists = False
2318 self.selection_V_exists = False
2320 # Get an ordered list of the vertices of Selection-U
2321 verts_ordered_U = []
2322 if self.selection_U_exists:
2323 verts_ordered_U = self.get_ordered_verts(
2324 self.main_object, all_selected_edges_idx,
2325 all_verts_idx, first_vert_U_idx,
2326 middle_vertex_idx, closing_vert_U_idx
2328 verts_ordered_U_indices = [x.index for x in verts_ordered_U]
2330 # Get an ordered list of the vertices of Selection-U2
2331 verts_ordered_U2 = []
2332 if self.selection_U2_exists:
2333 verts_ordered_U2 = self.get_ordered_verts(
2334 self.main_object, all_selected_edges_idx,
2335 all_verts_idx, first_vert_U2_idx,
2336 middle_vertex_idx, closing_vert_U2_idx
2338 verts_ordered_U2_indices = [x.index for x in verts_ordered_U2]
2340 # Get an ordered list of the vertices of Selection-V
2341 verts_ordered_V = []
2342 if self.selection_V_exists:
2343 verts_ordered_V = self.get_ordered_verts(
2344 self.main_object, all_selected_edges_idx,
2345 all_verts_idx, first_vert_V_idx,
2346 middle_vertex_idx, closing_vert_V_idx
2348 verts_ordered_V_indices = [x.index for x in verts_ordered_V]
2350 # Get an ordered list of the vertices of Selection-V2
2351 verts_ordered_V2 = []
2352 if self.selection_V2_exists:
2353 verts_ordered_V2 = self.get_ordered_verts(
2354 self.main_object, all_selected_edges_idx,
2355 all_verts_idx, first_vert_V2_idx,
2356 middle_vertex_idx, closing_vert_V2_idx
2358 verts_ordered_V2_indices = [x.index for x in verts_ordered_V2]
2360 # Check if when there are two-not-connected selections both have the same
2361 # number of verts. If not terminate the script
2362 if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or
2363 (self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
2364 # Display a warning
2365 self.report({'WARNING'}, "Both selections must have the same number of edges")
2367 self.cleanup_on_interruption()
2368 self.stopping_errors = True
2370 return{'CANCELLED'}
2372 # Calculate edges U proportions
2373 # Sum selected edges U lengths
2374 edges_lengths_U = []
2375 edges_lengths_sum_U = 0
2377 if self.selection_U_exists:
2378 edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(
2379 self.main_object,
2380 verts_ordered_U
2382 if self.selection_U2_exists:
2383 edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(
2384 self.main_object,
2385 verts_ordered_U2
2387 # Sum selected edges V lengths
2388 edges_lengths_V = []
2389 edges_lengths_sum_V = 0
2391 if self.selection_V_exists:
2392 edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(
2393 self.main_object,
2394 verts_ordered_V
2396 if self.selection_V2_exists:
2397 edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(
2398 self.main_object,
2399 verts_ordered_V2
2402 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2403 bpy.ops.curve.subdivide('INVOKE_REGION_WIN',
2404 number_cuts=bpy.context.scene.bsurfaces.SURFSK_precision)
2405 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2407 # Proportions U
2408 edges_proportions_U = []
2409 edges_proportions_U = self.get_edges_proportions(
2410 edges_lengths_U, edges_lengths_sum_U,
2411 self.selection_U_exists, self.edges_U
2413 verts_count_U = len(edges_proportions_U) + 1
2415 if self.selection_U2_exists:
2416 edges_proportions_U2 = []
2417 edges_proportions_U2 = self.get_edges_proportions(
2418 edges_lengths_U2, edges_lengths_sum_U2,
2419 self.selection_U2_exists, self.edges_V
2421 verts_count_U2 = len(edges_proportions_U2) + 1
2423 # Proportions V
2424 edges_proportions_V = []
2425 edges_proportions_V = self.get_edges_proportions(
2426 edges_lengths_V, edges_lengths_sum_V,
2427 self.selection_V_exists, self.edges_V
2429 verts_count_V = len(edges_proportions_V) + 1
2431 if self.selection_V2_exists:
2432 edges_proportions_V2 = []
2433 edges_proportions_V2 = self.get_edges_proportions(
2434 edges_lengths_V2, edges_lengths_sum_V2,
2435 self.selection_V2_exists, self.edges_V
2437 verts_count_V2 = len(edges_proportions_V2) + 1
2439 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2440 # the actual sketched curves with a "closing segment"
2441 if self.cyclic_follow and not self.selection_V_exists and not \
2442 ((self.selection_U_exists and not self.selection_U_is_closed) or
2443 (self.selection_U2_exists and not self.selection_U2_is_closed)):
2445 simplified_spline_coords = []
2446 simplified_curve = []
2447 ob_simplified_curve = []
2448 splines_first_v_co = []
2449 for i in range(len(self.main_splines.data.splines)):
2450 # Create a curve object for the actual spline "cyclic extension"
2451 simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
2452 ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
2453 bpy.context.collection.objects.link(ob_simplified_curve[i])
2455 simplified_curve[i].dimensions = "3D"
2457 spline_coords = []
2458 for bp in self.main_splines.data.splines[i].bezier_points:
2459 spline_coords.append(bp.co)
2461 # Simplification
2462 simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
2464 # Get the coordinates of the first vert of the actual spline
2465 splines_first_v_co.append(simplified_spline_coords[i][0])
2467 # Generate the spline
2468 spline = simplified_curve[i].splines.new('BEZIER')
2469 # less one because one point is added when the spline is created
2470 spline.bezier_points.add(len(simplified_spline_coords[i]) - 1)
2471 for p in range(0, len(simplified_spline_coords[i])):
2472 spline.bezier_points[p].co = simplified_spline_coords[i][p]
2474 spline.use_cyclic_u = True
2476 spline_bp_count = len(spline.bezier_points)
2478 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2479 ob_simplified_curve[i].select_set(True)
2480 bpy.context.view_layer.objects.active = ob_simplified_curve[i]
2482 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2483 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
2484 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2485 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
2486 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2488 # Select the "closing segment", and subdivide it
2489 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
2490 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
2491 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
2493 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
2494 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
2495 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
2497 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2498 segments = sqrt(
2499 (ob_simplified_curve[i].data.splines[0].bezier_points[0].co -
2500 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length /
2501 self.average_gp_segment_length
2503 for t in range(2):
2504 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=segments)
2506 # Delete the other vertices and make it non-cyclic to
2507 # keep only the needed verts of the "closing segment"
2508 bpy.ops.curve.select_all(action='INVERT')
2509 bpy.ops.curve.delete(type='VERT')
2510 ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
2511 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2513 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2514 first_new_index = len(self.main_splines.data.splines[i].bezier_points)
2515 self.main_splines.data.splines[i].bezier_points.add(
2516 len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1
2518 for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
2519 self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = \
2520 ob_simplified_curve[i].data.splines[0].bezier_points[t].co
2522 # Delete the temporal curve
2523 bpy.ops.object.delete({"selected_objects": [ob_simplified_curve[i]]})
2525 # Get the coords of the points distributed along the sketched strokes,
2526 # with proportions-U of the first selection
2527 pts_on_strokes_with_proportions_U = self.distribute_pts(
2528 self.main_splines.data.splines,
2529 edges_proportions_U
2531 sketched_splines_parsed = []
2533 if self.selection_U2_exists:
2534 # Initialize the multidimensional list with the proportions of all the segments
2535 proportions_loops_crossing_strokes = []
2536 for i in range(len(pts_on_strokes_with_proportions_U)):
2537 proportions_loops_crossing_strokes.append([])
2539 for t in range(len(pts_on_strokes_with_proportions_U[0])):
2540 proportions_loops_crossing_strokes[i].append(None)
2542 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2543 for lp in range(len(pts_on_strokes_with_proportions_U[0])):
2544 loop_segments_lengths = []
2546 for st in range(len(pts_on_strokes_with_proportions_U)):
2547 # When on the first stroke, add the segment from the selection to the dirst stroke
2548 if st == 0:
2549 loop_segments_lengths.append(
2550 ((self.main_object.matrix_world @ verts_ordered_U[lp].co) -
2551 pts_on_strokes_with_proportions_U[0][lp]).length
2553 # For all strokes except for the last, calculate the distance
2554 # from the actual stroke to the next
2555 if st != len(pts_on_strokes_with_proportions_U) - 1:
2556 loop_segments_lengths.append(
2557 (pts_on_strokes_with_proportions_U[st][lp] -
2558 pts_on_strokes_with_proportions_U[st + 1][lp]).length
2560 # When on the last stroke, add the segments
2561 # from the last stroke to the second selection
2562 if st == len(pts_on_strokes_with_proportions_U) - 1:
2563 loop_segments_lengths.append(
2564 (pts_on_strokes_with_proportions_U[st][lp] -
2565 (self.main_object.matrix_world @ verts_ordered_U2[lp].co)).length
2567 # Calculate full loop length
2568 loop_seg_lengths_sum = 0
2569 for i in range(len(loop_segments_lengths)):
2570 loop_seg_lengths_sum += loop_segments_lengths[i]
2572 # Fill the multidimensional list with the proportions of all the segments
2573 for st in range(len(pts_on_strokes_with_proportions_U)):
2574 proportions_loops_crossing_strokes[st][lp] = \
2575 loop_segments_lengths[st] / loop_seg_lengths_sum
2577 # Calculate proportions for each stroke
2578 for st in range(len(pts_on_strokes_with_proportions_U)):
2579 actual_stroke_spline = []
2580 # Needs to be a list for the "distribute_pts" method
2581 actual_stroke_spline.append(self.main_splines.data.splines[st])
2583 # Calculate the proportions for the actual stroke.
2584 actual_edges_proportions_U = []
2585 for i in range(len(edges_proportions_U)):
2586 proportions_sum = 0
2588 # Sum the proportions of this loop up to the actual.
2589 for t in range(0, st + 1):
2590 proportions_sum += proportions_loops_crossing_strokes[t][i]
2591 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2592 # and the proportions refer to edges, so we start at the element 1
2593 # of proportions_loops_crossing_strokes instead of element 0
2594 actual_edges_proportions_U.append(
2595 edges_proportions_U[i] -
2596 ((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)
2598 points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
2599 sketched_splines_parsed.append(points_actual_spline[0])
2600 else:
2601 sketched_splines_parsed = pts_on_strokes_with_proportions_U
2603 # If the selection type is "TWO_NOT_CONNECTED" replace the
2604 # points of the last spline with the points in the "target" selection
2605 if selection_type == "TWO_NOT_CONNECTED":
2606 if self.selection_U2_exists:
2607 for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
2608 sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = \
2609 self.main_object.matrix_world @ verts_ordered_U2[i].co
2611 # Create temporary curves along the "control-points" found
2612 # on the sketched curves and the mesh selection
2613 mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
2614 me = bpy.data.meshes.new(mesh_ctrl_pts_name)
2615 ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
2616 ob_ctrl_pts.data = me
2617 bpy.context.collection.objects.link(ob_ctrl_pts)
2619 cyclic_loops_U = []
2620 first_verts = []
2621 second_verts = []
2622 last_verts = []
2624 for i in range(0, verts_count_U):
2625 vert_num_in_spline = 1
2627 if self.selection_U_exists:
2628 ob_ctrl_pts.data.vertices.add(1)
2629 last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2630 last_v.co = self.main_object.matrix_world @ verts_ordered_U[i].co
2632 vert_num_in_spline += 1
2634 for t in range(0, len(sketched_splines_parsed)):
2635 ob_ctrl_pts.data.vertices.add(1)
2636 v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2637 v.co = sketched_splines_parsed[t][i]
2639 if vert_num_in_spline > 1:
2640 ob_ctrl_pts.data.edges.add(1)
2641 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = \
2642 len(ob_ctrl_pts.data.vertices) - 2
2643 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = \
2644 len(ob_ctrl_pts.data.vertices) - 1
2646 if t == 0:
2647 first_verts.append(v.index)
2649 if t == 1:
2650 second_verts.append(v.index)
2652 if t == len(sketched_splines_parsed) - 1:
2653 last_verts.append(v.index)
2655 last_v = v
2656 vert_num_in_spline += 1
2658 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2659 ob_ctrl_pts.select_set(True)
2660 bpy.context.view_layer.objects.active = ob_ctrl_pts
2662 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2663 bpy.ops.mesh.select_all(action='DESELECT')
2664 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2666 # Determine which loops-U will be "Cyclic"
2667 for i in range(0, len(first_verts)):
2668 # When there is Cyclic Cross there is no need of
2669 # Automatic Join, (and there are at least three strokes)
2670 if self.automatic_join and not self.cyclic_cross and \
2671 selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3:
2673 v = ob_ctrl_pts.data.vertices
2674 first_point_co = v[first_verts[i]].co
2675 second_point_co = v[second_verts[i]].co
2676 last_point_co = v[last_verts[i]].co
2678 # Coordinates of the point in the center of both the first and last verts.
2679 verts_center_co = [
2680 (first_point_co[0] + last_point_co[0]) / 2,
2681 (first_point_co[1] + last_point_co[1]) / 2,
2682 (first_point_co[2] + last_point_co[2]) / 2
2684 vec_A = second_point_co - first_point_co
2685 vec_B = second_point_co - Vector(verts_center_co)
2687 # Calculate the length of the first segment of the loop,
2688 # and the length it would have after moving the first vert
2689 # to the middle position between first and last
2690 length_original = (second_point_co - first_point_co).length
2691 length_target = (second_point_co - Vector(verts_center_co)).length
2693 angle = vec_A.angle(vec_B) / pi
2695 # If the target length doesn't stretch too much, and the
2696 # its angle doesn't change to much either
2697 if length_target <= length_original * 1.03 * self.join_stretch_factor and \
2698 angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists:
2700 cyclic_loops_U.append(True)
2701 # Move the first vert to the center coordinates
2702 ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
2703 # Select the last verts from Cyclic loops, for later deletion all at once
2704 v[last_verts[i]].select_set(True)
2705 else:
2706 cyclic_loops_U.append(False)
2707 else:
2708 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2709 if self.cyclic_cross and not self.selection_U_exists and not \
2710 ((self.selection_V_exists and not self.selection_V_is_closed) or
2711 (self.selection_V2_exists and not self.selection_V2_is_closed)):
2713 cyclic_loops_U.append(True)
2714 else:
2715 cyclic_loops_U.append(False)
2717 # The cyclic_loops_U list needs to be reversed.
2718 cyclic_loops_U.reverse()
2720 # Delete the previously selected (last_)verts.
2721 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2722 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
2723 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2725 # Create curves from control points.
2726 bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
2727 ob_curves_surf = bpy.context.view_layer.objects.active
2728 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2729 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2730 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2732 # Make Cyclic the splines designated as Cyclic.
2733 for i in range(0, len(cyclic_loops_U)):
2734 ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
2736 # Get the coords of all points on first loop-U, for later comparison with its
2737 # subdivided version, to know which points of the loops-U are crossed by the
2738 # original strokes. The indices will be the same for the other loops-U
2739 if self.loops_on_strokes:
2740 coords_loops_U_control_points = []
2741 for p in ob_ctrl_pts.data.splines[0].bezier_points:
2742 coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
2744 tuple(coords_loops_U_control_points)
2746 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2747 if self.loops_on_strokes and not self.selection_V_exists:
2748 edges_V_count = len(self.main_splines.data.splines) * self.edges_V
2749 else:
2750 edges_V_count = len(edges_proportions_V)
2752 # The Follow precision will vary depending on the number of Follow face-loops
2753 precision_multiplier = round(2 + (edges_V_count / 15))
2754 curve_cuts = bpy.context.scene.bsurfaces.SURFSK_precision * precision_multiplier
2756 # Subdivide the curves
2757 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=curve_cuts)
2759 # The verts position shifting that happens with splines subdivision.
2760 # For later reorder splines points
2761 verts_position_shift = curve_cuts + 1
2762 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2764 # Reorder coordinates of the points of each spline to put the first point of
2765 # the spline starting at the position it was the first point before sudividing
2766 # the curve. And make a new curve object per spline (to handle memory better later)
2767 splines_U_objects = []
2768 for i in range(len(ob_curves_surf.data.splines)):
2769 spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
2770 ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
2771 bpy.context.collection.objects.link(ob_spline_U)
2773 spline_U_curve.dimensions = "3D"
2775 # Add points to the spline in the new curve object
2776 ob_spline_U.data.splines.new('BEZIER')
2777 for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
2778 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2779 if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
2780 point_index = t + verts_position_shift
2781 else:
2782 point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
2783 else:
2784 point_index = t
2785 # to avoid adding the first point since it's added when the spline is created
2786 if t > 0:
2787 ob_spline_U.data.splines[0].bezier_points.add(1)
2788 ob_spline_U.data.splines[0].bezier_points[t].co = \
2789 ob_curves_surf.data.splines[i].bezier_points[point_index].co
2791 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2792 # Add a last point at the same location as the first one
2793 ob_spline_U.data.splines[0].bezier_points.add(1)
2794 ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = \
2795 ob_spline_U.data.splines[0].bezier_points[0].co
2796 else:
2797 ob_spline_U.data.splines[0].use_cyclic_u = False
2799 splines_U_objects.append(ob_spline_U)
2800 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2801 ob_spline_U.select_set(True)
2802 bpy.context.view_layer.objects.active = ob_spline_U
2804 # When option "Loops on strokes" is active each "Cross" loop will have
2805 # its own proportions according to where the original strokes "touch" them
2806 if self.loops_on_strokes:
2807 # Get the indices of points where the original strokes "touch" loops-U
2808 points_U_crossed_by_strokes = []
2809 for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
2810 bp = splines_U_objects[0].data.splines[0].bezier_points[i]
2811 if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
2812 points_U_crossed_by_strokes.append(i)
2814 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2815 edge_order_number_for_splines = {}
2816 if self.selection_V_exists:
2817 # For two-connected selections add a first hypothetic stroke at the beginning.
2818 if selection_type == "TWO_CONNECTED":
2819 edge_order_number_for_splines[0] = 0
2821 for i in range(len(self.main_splines.data.splines)):
2822 sp = self.main_splines.data.splines[i]
2823 v_idx, dist_temp = self.shortest_distance(
2824 self.main_object,
2825 sp.bezier_points[0].co,
2826 verts_ordered_V_indices
2828 # Get the position (edges count) of the vert v_idx in the selected chain V
2829 edge_idx_in_chain = verts_ordered_V_indices.index(v_idx)
2831 # For two-connected selections the strokes go after the
2832 # hypothetic stroke added before, so the index adds one per spline
2833 if selection_type == "TWO_CONNECTED":
2834 spline_number = i + 1
2835 else:
2836 spline_number = i
2838 edge_order_number_for_splines[spline_number] = edge_idx_in_chain
2840 # Get the first and last verts indices for later comparison
2841 if i == 0:
2842 first_v_idx = v_idx
2843 elif i == len(self.main_splines.data.splines) - 1:
2844 last_v_idx = v_idx
2846 if self.selection_V_is_closed:
2847 # If there is no last stroke on the last vertex (same as first vertex),
2848 # add a hypothetic spline at last vert order
2849 if first_v_idx != last_v_idx:
2850 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2851 len(verts_ordered_V_indices) - 1
2852 else:
2853 if self.cyclic_cross:
2854 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2855 len(verts_ordered_V_indices) - 2
2856 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2857 len(verts_ordered_V_indices) - 1
2858 else:
2859 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2860 len(verts_ordered_V_indices) - 1
2862 # Get the coords of the points distributed along the
2863 # "crossing curves", with appropriate proportions-V
2864 surface_splines_parsed = []
2865 for i in range(len(splines_U_objects)):
2866 sp_ob = splines_U_objects[i]
2867 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2868 if self.loops_on_strokes:
2869 # Segments distances from stroke to stroke
2870 dist = 0
2871 full_dist = 0
2872 segments_distances = []
2873 for t in range(len(sp_ob.data.splines[0].bezier_points)):
2874 bp = sp_ob.data.splines[0].bezier_points[t]
2876 if t == 0:
2877 last_p = bp.co
2878 else:
2879 actual_p = bp.co
2880 dist += (last_p - actual_p).length
2882 if t in points_U_crossed_by_strokes:
2883 segments_distances.append(dist)
2884 full_dist += dist
2886 dist = 0
2888 last_p = actual_p
2890 # Calculate Proportions.
2891 used_edges_proportions_V = []
2892 for t in range(len(segments_distances)):
2893 if self.selection_V_exists:
2894 if t == 0:
2895 order_number_last_stroke = 0
2897 segment_edges_length_V = 0
2898 segment_edges_length_V2 = 0
2899 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2900 segment_edges_length_V += edges_lengths_V[order]
2901 if self.selection_V2_exists:
2902 segment_edges_length_V2 += edges_lengths_V2[order]
2904 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2905 # Calculate each "sub-segment" (the ones between each stroke) length
2906 if self.selection_V2_exists:
2907 proportion_sub_seg = (edges_lengths_V2[order] -
2908 ((edges_lengths_V2[order] - edges_lengths_V[order]) /
2909 len(splines_U_objects) * i)) / (segment_edges_length_V2 -
2910 (segment_edges_length_V2 - segment_edges_length_V) /
2911 len(splines_U_objects) * i)
2913 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2914 else:
2915 proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
2916 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2918 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2920 order_number_last_stroke = edge_order_number_for_splines[t + 1]
2922 else:
2923 for c in range(self.edges_V):
2924 # Calculate each "sub-segment" (the ones between each stroke) length
2925 sub_seg_dist = segments_distances[t] / self.edges_V
2926 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2928 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2929 surface_splines_parsed.append(actual_spline[0])
2931 else:
2932 if self.selection_V2_exists:
2933 used_edges_proportions_V = []
2934 for p in range(len(edges_proportions_V)):
2935 used_edges_proportions_V.append(
2936 edges_proportions_V2[p] -
2937 ((edges_proportions_V2[p] -
2938 edges_proportions_V[p]) / len(splines_U_objects) * i)
2940 else:
2941 used_edges_proportions_V = edges_proportions_V
2943 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2944 surface_splines_parsed.append(actual_spline[0])
2946 # Set the verts of the first and last splines to the locations
2947 # of the respective verts in the selections
2948 if self.selection_V_exists:
2949 for i in range(0, len(surface_splines_parsed[0])):
2950 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = \
2951 self.main_object.matrix_world @ verts_ordered_V[i].co
2953 if selection_type == "TWO_NOT_CONNECTED":
2954 if self.selection_V2_exists:
2955 for i in range(0, len(surface_splines_parsed[0])):
2956 surface_splines_parsed[0][i] = self.main_object.matrix_world @ verts_ordered_V2[i].co
2958 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2959 # merge the verts of the tips of the loops when they are "near enough"
2960 if self.automatic_join and selection_type != "TWO_CONNECTED":
2961 # Join the tips of "Follow" loops that are near enough and must be "closed"
2962 if not self.selection_V_exists and len(edges_proportions_U) >= 3:
2963 for i in range(len(surface_splines_parsed[0])):
2964 sp = surface_splines_parsed
2965 loop_segment_dist = (sp[0][i] - sp[1][i]).length
2966 full_loop_dist = loop_segment_dist * self.edges_U
2968 verts_middle_position_co = [
2969 (sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2,
2970 (sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2,
2971 (sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2
2973 points_original = []
2974 points_original.append(sp[1][i])
2975 points_original.append(sp[0][i])
2977 points_target = []
2978 points_target.append(sp[1][i])
2979 points_target.append(Vector(verts_middle_position_co))
2981 vec_A = points_original[0] - points_original[1]
2982 vec_B = points_target[0] - points_target[1]
2983 # check for zero angles, not sure if it is a great fix
2984 if vec_A.length != 0 and vec_B.length != 0:
2985 angle = vec_A.angle(vec_B) / pi
2986 edge_new_length = (Vector(verts_middle_position_co) - sp[1][i]).length
2987 else:
2988 angle = 0
2989 edge_new_length = 0
2991 # If after moving the verts to the middle point, the segment doesn't stretch too much
2992 if edge_new_length <= loop_segment_dist * 1.5 * \
2993 self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor:
2995 # Avoid joining when the actual loop must be merged with the original mesh
2996 if not (self.selection_U_exists and i == 0) and \
2997 not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1):
2999 # Change the coords of both verts to the middle position
3000 surface_splines_parsed[0][i] = verts_middle_position_co
3001 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
3003 # Delete object with control points and object from grease pencil conversion
3004 bpy.ops.object.delete({"selected_objects": [ob_ctrl_pts]})
3006 bpy.ops.object.delete({"selected_objects": splines_U_objects})
3008 # Generate surface
3010 # Get all verts coords
3011 all_surface_verts_co = []
3012 for i in range(0, len(surface_splines_parsed)):
3013 # Get coords of all verts and make a list with them
3014 for pt_co in surface_splines_parsed[i]:
3015 all_surface_verts_co.append(pt_co)
3017 # Define verts for each face
3018 all_surface_faces = []
3019 for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
3020 if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
3021 all_surface_faces.append(
3022 [i + 1, i, i + len(surface_splines_parsed[0]),
3023 i + len(surface_splines_parsed[0]) + 1]
3025 # Build the mesh
3026 surf_me_name = "SURFSKIO_surface"
3027 me_surf = bpy.data.meshes.new(surf_me_name)
3029 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
3031 me_surf.update()
3033 ob_surface = bpy.data.objects.new(surf_me_name, me_surf)
3034 bpy.context.collection.objects.link(ob_surface)
3036 # Select all the "unselected but participating" verts, from closed selection
3037 # or double selections with middle-vertex, for later join with remove doubles
3038 for v_idx in single_unselected_verts:
3039 self.main_object.data.vertices[v_idx].select_set(True)
3041 # Join the new mesh to the main object
3042 ob_surface.select_set(True)
3043 self.main_object.select_set(True)
3044 bpy.context.view_layer.objects.active = self.main_object
3046 bpy.ops.object.join('INVOKE_REGION_WIN')
3048 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3050 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
3051 bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
3052 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
3054 return{'FINISHED'}
3056 def execute(self, context):
3058 bsurfaces_props = bpy.context.scene.bsurfaces
3060 bsurfaces_props.SURFSK_object_with_strokes.select_set(True)
3061 self.main_object = bsurfaces_props.SURFSK_object_with_retopology
3062 self.main_object.select_set(True)
3063 bpy.context.view_layer.objects.active = self.main_object
3065 bpy.context.preferences.edit.use_global_undo = False
3067 if not self.is_fill_faces:
3068 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3069 value='True, False, False')
3071 # Build splines from the "last saved splines".
3072 last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
3073 self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
3074 bpy.context.collection.objects.link(self.main_splines)
3076 last_saved_curve.dimensions = "3D"
3078 for sp in self.last_strokes_splines_coords:
3079 spline = self.main_splines.data.splines.new('BEZIER')
3080 # less one because one point is added when the spline is created
3081 spline.bezier_points.add(len(sp) - 1)
3082 for p in range(0, len(sp)):
3083 spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
3085 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3086 bpy.ops.object.mode_set(mode='OBJECT')
3088 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3089 self.main_splines.select_set(True)
3090 bpy.context.view_layer.objects.active = self.main_splines
3092 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3093 bpy.ops.object.mode_set(mode='EDIT')
3095 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3096 # Important to make it vector first and then automatic, otherwise the
3097 # tips handles get too big and distort the shrinkwrap results later
3098 bpy.ops.curve.handle_type_set(type='VECTOR')
3099 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3100 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3101 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3103 self.main_splines.name = "SURFSKIO_temp_strokes"
3105 if self.is_crosshatch:
3106 strokes_for_crosshatch = True
3107 strokes_for_rectangular_surface = False
3108 else:
3109 strokes_for_rectangular_surface = True
3110 strokes_for_crosshatch = False
3112 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3113 self.main_object.select_set(True)
3114 bpy.context.view_layer.objects.active = self.main_object
3116 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3118 if strokes_for_rectangular_surface:
3119 self.rectangular_surface()
3120 elif strokes_for_crosshatch:
3121 self.crosshatch_surface_execute()
3123 # Delete main splines
3124 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3125 bpy.ops.object.delete({"selected_objects": [self.main_splines]})
3127 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3128 self.main_object.select_set(True)
3129 bpy.context.view_layer.objects.active = self.main_object
3131 # Delete grease pencil strokes
3132 if self.strokes_type == "GP_STROKES" and not self.stopping_errors and not self.keep_strokes:
3133 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers[0].clear()
3135 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3137 bpy.context.preferences.edit.use_global_undo = self.initial_global_undo_state
3139 return{'FINISHED'}
3141 def invoke(self, context, event):
3142 self.initial_global_undo_state = bpy.context.preferences.edit.use_global_undo
3144 bsurfaces_props = bpy.context.scene.bsurfaces
3145 self.cyclic_cross = bsurfaces_props.SURFSK_cyclic_cross
3146 self.cyclic_follow = bsurfaces_props.SURFSK_cyclic_follow
3147 self.automatic_join = bsurfaces_props.SURFSK_automatic_join
3148 self.loops_on_strokes = bsurfaces_props.SURFSK_loops_on_strokes
3149 self.keep_strokes = bsurfaces_props.SURFSK_keep_strokes
3151 try:
3152 bsurfaces_props.SURFSK_object_with_strokes.select_set(True)
3153 except:
3154 self.report({'WARNING'}, "Specify the name of the object with retopology")
3155 return{"CANCELLED"}
3157 self.main_object = bsurfaces_props.SURFSK_object_with_retopology
3158 self.main_object.select_set(True)
3159 bpy.context.view_layer.objects.active = self.main_object
3161 self.main_object_selected_verts_count = int(self.main_object.data.total_vert_sel)
3163 bpy.context.preferences.edit.use_global_undo = False
3164 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3165 value='True, False, False')
3167 self.edges_U = 5
3169 if self.loops_on_strokes:
3170 self.edges_V = 1
3171 else:
3172 self.edges_V = 5
3174 self.is_fill_faces = False
3175 self.stopping_errors = False
3176 self.last_strokes_splines_coords = []
3178 # Determine the type of the strokes
3179 self.strokes_type = get_strokes_type()
3181 # Check if it will be used grease pencil strokes or curves
3182 # If there are strokes to be used
3183 if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE":
3184 if self.strokes_type == "GP_STROKES":
3185 # Convert grease pencil strokes to curve
3186 gp = bsurfaces_props.SURFSK_object_with_strokes
3187 #bpy.ops.gpencil.convert(type='CURVE', use_link_strokes=False)
3188 self.original_curve = conver_gpencil_to_curve(context, gp)
3189 # XXX gpencil.convert now keep org object as active/selected, *not* newly created curve!
3190 # XXX This is far from perfect, but should work in most cases...
3191 # self.original_curve = bpy.context.object
3192 gplayer_prefix_translated = bpy.app.translations.pgettext_data('GP_Layer')
3193 for ob in bpy.context.selected_objects:
3194 if ob != bpy.context.view_layer.objects.active and \
3195 ob.name.startswith((gplayer_prefix_translated, 'GP_Layer')):
3196 self.original_curve = ob
3197 self.using_external_curves = False
3199 elif self.strokes_type == "EXTERNAL_CURVE":
3200 for ob in bpy.context.selected_objects:
3201 if ob != bpy.context.view_layer.objects.active:
3202 self.original_curve = ob
3203 self.using_external_curves = True
3205 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3207 # Make sure there are no objects left from erroneous
3208 # executions of this operator, with the reserved names used here
3209 for o in bpy.data.objects:
3210 if o.name.find("SURFSKIO_") != -1:
3211 bpy.ops.object.delete({"selected_objects": [o]})
3213 #bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3214 self.original_curve.select_set(True)
3215 bpy.context.view_layer.objects.active = self.original_curve
3217 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3219 self.temporary_curve = bpy.context.view_layer.objects.active
3221 # Deselect all points of the curve
3222 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3223 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3224 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3226 # Delete splines with only a single isolated point
3227 for i in range(len(self.temporary_curve.data.splines)):
3228 sp = self.temporary_curve.data.splines[i]
3230 if len(sp.bezier_points) == 1:
3231 sp.bezier_points[0].select_control_point = True
3233 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3234 bpy.ops.curve.delete(type='VERT')
3235 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3237 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3238 self.temporary_curve.select_set(True)
3239 bpy.context.view_layer.objects.active = self.temporary_curve
3241 # Set a minimum number of points for crosshatch
3242 minimum_points_num = 15
3244 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3245 # Check if the number of points of each curve has at least the number of points
3246 # of minimum_points_num, which is a bit more than the face-loops limit.
3247 # If not, subdivide to reach at least that number of points
3248 for i in range(len(self.temporary_curve.data.splines)):
3249 sp = self.temporary_curve.data.splines[i]
3251 if len(sp.bezier_points) < minimum_points_num:
3252 for bp in sp.bezier_points:
3253 bp.select_control_point = True
3255 if (len(sp.bezier_points) - 1) != 0:
3256 # Formula to get the number of cuts that will make a curve
3257 # of N number of points have near to "minimum_points_num"
3258 # points, when subdividing with this number of cuts
3259 subdivide_cuts = int(
3260 (minimum_points_num - len(sp.bezier_points)) /
3261 (len(sp.bezier_points) - 1)
3262 ) + 1
3263 else:
3264 subdivide_cuts = 0
3266 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3267 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3269 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3271 # Detect if the strokes are a crosshatch and do it if it is
3272 self.crosshatch_surface_invoke(self.temporary_curve)
3274 if not self.is_crosshatch:
3275 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3276 self.temporary_curve.select_set(True)
3277 bpy.context.view_layer.objects.active = self.temporary_curve
3279 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3281 # Set a minimum number of points for rectangular surfaces
3282 minimum_points_num = 60
3284 # Check if the number of points of each curve has at least the number of points
3285 # of minimum_points_num, which is a bit more than the face-loops limit.
3286 # If not, subdivide to reach at least that number of points
3287 for i in range(len(self.temporary_curve.data.splines)):
3288 sp = self.temporary_curve.data.splines[i]
3290 if len(sp.bezier_points) < minimum_points_num:
3291 for bp in sp.bezier_points:
3292 bp.select_control_point = True
3294 if (len(sp.bezier_points) - 1) != 0:
3295 # Formula to get the number of cuts that will make a curve of
3296 # N number of points have near to "minimum_points_num" points,
3297 # when subdividing with this number of cuts
3298 subdivide_cuts = int(
3299 (minimum_points_num - len(sp.bezier_points)) /
3300 (len(sp.bezier_points) - 1)
3301 ) + 1
3302 else:
3303 subdivide_cuts = 0
3305 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3306 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3308 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3310 # Save coordinates of the actual strokes (as the "last saved splines")
3311 for sp_idx in range(len(self.temporary_curve.data.splines)):
3312 self.last_strokes_splines_coords.append([])
3313 for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
3314 coords = self.temporary_curve.matrix_world @ \
3315 self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
3316 self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
3318 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3319 for sp_idx in range(len(self.temporary_curve.data.splines)):
3320 if self.temporary_curve.data.splines[sp_idx].use_cyclic_u is True:
3321 first_p_co = self.last_strokes_splines_coords[sp_idx][0]
3322 last_p_co = self.last_strokes_splines_coords[sp_idx][
3323 len(self.last_strokes_splines_coords[sp_idx]) - 1
3325 target_co = [
3326 (first_p_co[0] + last_p_co[0]) / 2,
3327 (first_p_co[1] + last_p_co[1]) / 2,
3328 (first_p_co[2] + last_p_co[2]) / 2
3331 self.last_strokes_splines_coords[sp_idx][0] = target_co
3332 self.last_strokes_splines_coords[sp_idx][
3333 len(self.last_strokes_splines_coords[sp_idx]) - 1
3334 ] = target_co
3335 tuple(self.last_strokes_splines_coords)
3337 # Estimation of the average length of the segments between
3338 # each point of the grease pencil strokes.
3339 # Will be useful to determine whether a curve should be made "Cyclic"
3340 segments_lengths_sum = 0
3341 segments_count = 0
3342 random_spline = self.temporary_curve.data.splines[0].bezier_points
3343 for i in range(0, len(random_spline)):
3344 if i != 0 and len(random_spline) - 1 >= i:
3345 segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
3346 segments_count += 1
3348 self.average_gp_segment_length = segments_lengths_sum / segments_count
3350 # Delete temporary strokes curve object
3351 bpy.ops.object.delete({"selected_objects": [self.temporary_curve]})
3353 #bpy.context.preferences.edit.use_global_undo = False
3355 # If "Keep strokes" option is not active, delete original strokes curve object
3356 if (not self.stopping_errors and not self.keep_strokes) or self.is_crosshatch:
3357 bpy.ops.object.delete({"selected_objects": [self.original_curve]})
3359 # Delete grease pencil strokes
3360 if self.strokes_type == "GP_STROKES" and not self.stopping_errors and not self.keep_strokes:
3361 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers[0].clear()
3363 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3364 self.main_object.select_set(True)
3365 bpy.context.view_layer.objects.active = self.main_object
3367 # Set again since "execute()" will turn it again to its initial value
3368 #bpy.ops.gpencil.surfsk_add_surface()
3369 self.execute(context)
3371 bpy.context.preferences.edit.use_global_undo = self.initial_global_undo_state
3373 if not self.stopping_errors:
3374 return {"FINISHED"}
3375 else:
3376 return{"CANCELLED"}
3378 elif self.strokes_type == "SELECTION_ALONE":
3379 self.is_fill_faces = True
3380 created_faces_count = self.fill_with_faces(self.main_object)
3382 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3383 bpy.context.preferences.edit.use_global_undo = self.initial_global_undo_state
3385 if created_faces_count == 0:
3386 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3387 return {"CANCELLED"}
3388 else:
3389 return {"FINISHED"}
3391 bpy.context.preferences.edit.use_global_undo = self.initial_global_undo_state
3393 if self.strokes_type == "EXTERNAL_NO_CURVE":
3394 self.report({'WARNING'}, "The secondary object is not a Curve.")
3395 return{"CANCELLED"}
3397 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3398 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3399 return{"CANCELLED"}
3401 elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or \
3402 self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3404 self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3405 return{"CANCELLED"}
3407 elif self.strokes_type == "NO_STROKES":
3408 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3409 return{"CANCELLED"}
3411 elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
3412 self.report({'WARNING'}, "All splines must be Bezier.")
3413 return{"CANCELLED"}
3415 else:
3416 return{"CANCELLED"}
3418 # Edit strokes operator
3419 class GPENCIL_OT_SURFSK_init(Operator):
3420 bl_idname = "gpencil.surfsk_init"
3421 bl_label = "Bsurfaces initialize"
3422 bl_description = "Bsurfaces initialiaze"
3424 def execute(self, context):
3425 #bpy.ops.object.mode_set(mode='OBJECT')
3426 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3427 mesh = bpy.data.meshes.new('BSurfaceMesh')
3428 mesh_object = object_utils.object_data_add(context, mesh, operator=None)
3429 mesh_object.select_set(True)
3430 bpy.context.view_layer.objects.active = mesh_object
3431 bpy.ops.object.modifier_add(type='SHRINKWRAP')
3433 bpy.ops.object.gpencil_add(radius=1.0, view_align=False, location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), type='EMPTY')
3434 gpencil_object = bpy.context.scene.objects[bpy.context.scene.objects[-1].name]
3435 gpencil_object.select_set(True)
3436 bpy.context.view_layer.objects.active = gpencil_object
3437 bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
3439 bpy.context.scene.bsurfaces.SURFSK_object_with_retopology = mesh_object
3440 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes = gpencil_object
3442 return{"FINISHED"}
3444 # Edit strokes operator
3445 class GPENCIL_OT_SURFSK_add_strokes(Operator):
3446 bl_idname = "gpencil.surfsk_add_strokes"
3447 bl_label = "Bsurfaces add strokes"
3448 bl_description = "Add the grease pencil strokes"
3450 def execute(self, context):
3451 # Determine the type of the strokes
3452 self.strokes_type = get_strokes_type()
3453 # Check if strokes are grease pencil strokes or a curves object
3454 selected_objs = bpy.context.selected_objects
3455 if self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3456 for ob in selected_objs:
3457 if ob != bpy.context.view_layer.objects.active:
3458 curve_ob = ob
3460 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3462 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3463 curve_ob.select_set(True)
3464 bpy.context.view_layer.objects.active = curve_ob
3466 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3467 else:
3468 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.select_set(True)
3469 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_object_with_strokes
3470 bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
3472 return{"FINISHED"}
3474 def invoke(self, context, event):
3475 try:
3476 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.select_set(True)
3477 except:
3478 self.report({'WARNING'}, "Specify the name of the object with strokes")
3479 return{"CANCELLED"}
3481 self.execute(context)
3483 return {"FINISHED"}
3486 # Edit strokes operator
3487 class GPENCIL_OT_SURFSK_edit_strokes(Operator):
3488 bl_idname = "gpencil.surfsk_edit_strokes"
3489 bl_label = "Bsurfaces edit strokes"
3490 bl_description = "Edit the grease pencil strokes or curves used"
3492 def execute(self, context):
3493 # Determine the type of the strokes
3494 self.strokes_type = get_strokes_type()
3495 # Check if strokes are grease pencil strokes or a curves object
3496 selected_objs = bpy.context.selected_objects
3497 if self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3498 for ob in selected_objs:
3499 if ob != bpy.context.view_layer.objects.active:
3500 curve_ob = ob
3502 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3504 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3505 curve_ob.select_set(True)
3506 bpy.context.view_layer.objects.active = curve_ob
3508 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3509 elif self.strokes_type == "GP_STROKES" or self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION":
3510 # Convert grease pencil strokes to curve
3511 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3512 #bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3513 gp = bpy.context.scene.bsurfaces.SURFSK_object_with_strokes
3514 conver_gpencil_to_curve(context, gp)
3515 for ob in bpy.context.selected_objects:
3516 if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
3517 ob_gp_strokes = ob
3519 ob_gp_strokes = bpy.context.object
3521 # Delete grease pencil strokes
3522 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers[0].clear()
3524 # Clean up curves
3525 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3526 ob_gp_strokes.select_set(True)
3527 bpy.context.view_layer.objects.active = ob_gp_strokes
3529 curve_crv = ob_gp_strokes.data
3530 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3531 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type="BEZIER")
3532 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type="AUTOMATIC")
3533 #curve_crv.show_handles = False
3534 #curve_crv.show_normal_face = False
3536 elif self.strokes_type == "EXTERNAL_NO_CURVE":
3537 self.report({'WARNING'}, "The secondary object is not a Curve.")
3538 return{"CANCELLED"}
3540 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3541 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3542 return{"CANCELLED"}
3544 elif self.strokes_type == "NO_STROKES" or self.strokes_type == "SELECTION_ALONE":
3545 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3546 return{"CANCELLED"}
3548 else:
3549 return{"CANCELLED"}
3551 def invoke(self, context, event):
3552 try:
3553 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.select_set(True)
3554 except:
3555 self.report({'WARNING'}, "Specify the name of the object with strokes")
3556 return{"CANCELLED"}
3558 self.execute(context)
3560 return {"FINISHED"}
3563 class CURVE_OT_SURFSK_reorder_splines(Operator):
3564 bl_idname = "curve.surfsk_reorder_splines"
3565 bl_label = "Bsurfaces reorder splines"
3566 bl_description = "Defines the order of the splines by using grease pencil strokes"
3567 bl_options = {'REGISTER', 'UNDO'}
3569 def execute(self, context):
3570 objects_to_delete = []
3571 # Convert grease pencil strokes to curve.
3572 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3573 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3574 for ob in bpy.context.selected_objects:
3575 if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
3576 GP_strokes_curve = ob
3578 # GP_strokes_curve = bpy.context.object
3579 objects_to_delete.append(GP_strokes_curve)
3581 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3582 GP_strokes_curve.select_set(True)
3583 bpy.context.view_layer.objects.active = GP_strokes_curve
3585 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3586 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3587 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=100)
3588 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3590 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3591 GP_strokes_mesh = bpy.context.object
3592 objects_to_delete.append(GP_strokes_mesh)
3594 GP_strokes_mesh.data.resolution_u = 1
3595 bpy.ops.object.convert(target='MESH', keep_original=False)
3597 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3598 self.main_curve.select_set(True)
3599 bpy.context.view_layer.objects.active = self.main_curve
3601 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3602 curves_duplicate_1 = bpy.context.object
3603 objects_to_delete.append(curves_duplicate_1)
3605 minimum_points_num = 500
3607 # Some iterations since the subdivision operator
3608 # has a limit of 100 subdivisions per iteration
3609 for x in range(round(minimum_points_num / 100)):
3610 # Check if the number of points of each curve has at least the number of points
3611 # of minimum_points_num. If not, subdivide to reach at least that number of points
3612 for i in range(len(curves_duplicate_1.data.splines)):
3613 sp = curves_duplicate_1.data.splines[i]
3615 if len(sp.bezier_points) < minimum_points_num:
3616 for bp in sp.bezier_points:
3617 bp.select_control_point = True
3619 if (len(sp.bezier_points) - 1) != 0:
3620 # Formula to get the number of cuts that will make a curve of N
3621 # number of points have near to "minimum_points_num" points,
3622 # when subdividing with this number of cuts
3623 subdivide_cuts = int(
3624 (minimum_points_num - len(sp.bezier_points)) /
3625 (len(sp.bezier_points) - 1)
3626 ) + 1
3627 else:
3628 subdivide_cuts = 0
3630 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3631 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3632 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3633 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3635 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3636 curves_duplicate_2 = bpy.context.object
3637 objects_to_delete.append(curves_duplicate_2)
3639 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3640 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3641 curves_duplicate_2.select_set(True)
3642 bpy.context.view_layer.objects.active = curves_duplicate_2
3644 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
3645 curves_duplicate_2.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX"
3646 curves_duplicate_2.modifiers["Shrinkwrap"].target = GP_strokes_mesh
3647 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier='Shrinkwrap')
3649 # Get the distance of each vert from its original position to its position with Shrinkwrap
3650 nearest_points_coords = {}
3651 for st_idx in range(len(curves_duplicate_1.data.splines)):
3652 for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
3653 bp_1_co = curves_duplicate_1.matrix_world @ \
3654 curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
3656 bp_2_co = curves_duplicate_2.matrix_world @ \
3657 curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
3659 if bp_idx == 0:
3660 shortest_dist = (bp_1_co - bp_2_co).length
3661 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3662 "%.4f" % bp_2_co[1],
3663 "%.4f" % bp_2_co[2])
3665 dist = (bp_1_co - bp_2_co).length
3667 if dist < shortest_dist:
3668 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3669 "%.4f" % bp_2_co[1],
3670 "%.4f" % bp_2_co[2])
3671 shortest_dist = dist
3673 # Get all coords of GP strokes points, for comparison
3674 GP_strokes_coords = []
3675 for st_idx in range(len(GP_strokes_curve.data.splines)):
3676 GP_strokes_coords.append(
3677 [("%.4f" % x if "%.4f" % x != "-0.00" else "0.00",
3678 "%.4f" % y if "%.4f" % y != "-0.00" else "0.00",
3679 "%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for
3680 x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]]
3683 # Check the point of the GP strokes with the same coords as
3684 # the nearest points of the curves (with shrinkwrap)
3686 # Dictionary with GP stroke index as index, and a list as value.
3687 # The list has as index the point index of the GP stroke
3688 # nearest to the spline, and as value the spline index
3689 GP_connection_points = {}
3690 for gp_st_idx in range(len(GP_strokes_coords)):
3691 GPvert_spline_relationship = {}
3693 for splines_st_idx in range(len(nearest_points_coords)):
3694 if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
3695 GPvert_spline_relationship[
3696 GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])
3697 ] = splines_st_idx
3699 GP_connection_points[gp_st_idx] = GPvert_spline_relationship
3701 # Get the splines new order
3702 splines_new_order = []
3703 for i in GP_connection_points:
3704 dict_keys = sorted(GP_connection_points[i].keys()) # Sort dictionaries by key
3706 for k in dict_keys:
3707 splines_new_order.append(GP_connection_points[i][k])
3709 # Reorder
3710 curve_original_name = self.main_curve.name
3712 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3713 self.main_curve.select_set(True)
3714 bpy.context.view_layer.objects.active = self.main_curve
3716 self.main_curve.name = "SURFSKIO_CRV_ORD"
3718 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3719 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3720 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3722 for sp_idx in range(len(self.main_curve.data.splines)):
3723 self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
3725 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3726 bpy.ops.curve.separate('EXEC_REGION_WIN')
3727 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3729 # Get the names of the separated splines objects in the original order
3730 splines_unordered = {}
3731 for o in bpy.data.objects:
3732 if o.name.find("SURFSKIO_CRV_ORD") != -1:
3733 spline_order_string = o.name.partition(".")[2]
3735 if spline_order_string != "" and int(spline_order_string) > 0:
3736 spline_order_index = int(spline_order_string) - 1
3737 splines_unordered[spline_order_index] = o.name
3739 # Join all splines objects in final order
3740 for order_idx in splines_new_order:
3741 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3742 bpy.data.objects[splines_unordered[order_idx]].select_set(True)
3743 bpy.data.objects["SURFSKIO_CRV_ORD"].select_set(True)
3744 bpy.context.view_layer.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
3746 bpy.ops.object.join('INVOKE_REGION_WIN')
3748 # Go back to the original name of the curves object.
3749 bpy.context.object.name = curve_original_name
3751 # Delete all unused objects
3752 bpy.ops.object.delete({"selected_objects": objects_to_delete})
3754 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3755 bpy.data.objects[curve_original_name].select_set(True)
3756 bpy.context.view_layer.objects.active = bpy.data.objects[curve_original_name]
3758 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3759 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3761 bpy.context.scene.bsurfaces.SURFSK_object_with_strokes.data.layers[0].clear()
3763 return {"FINISHED"}
3765 def invoke(self, context, event):
3766 self.main_curve = bpy.context.object
3767 there_are_GP_strokes = False
3769 try:
3770 # Get the active grease pencil layer
3771 strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
3773 if strokes_num > 0:
3774 there_are_GP_strokes = True
3775 except:
3776 pass
3778 if there_are_GP_strokes:
3779 self.execute(context)
3780 self.report({'INFO'}, "Splines have been reordered")
3781 else:
3782 self.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
3784 return {"FINISHED"}
3787 class CURVE_OT_SURFSK_first_points(Operator):
3788 bl_idname = "curve.surfsk_first_points"
3789 bl_label = "Bsurfaces set first points"
3790 bl_description = "Set the selected points as the first point of each spline"
3791 bl_options = {'REGISTER', 'UNDO'}
3793 def execute(self, context):
3794 splines_to_invert = []
3796 # Check non-cyclic splines to invert
3797 for i in range(len(self.main_curve.data.splines)):
3798 b_points = self.main_curve.data.splines[i].bezier_points
3800 if i not in self.cyclic_splines: # Only for non-cyclic splines
3801 if b_points[len(b_points) - 1].select_control_point:
3802 splines_to_invert.append(i)
3804 # Reorder points of cyclic splines, and set all handles to "Automatic"
3806 # Check first selected point
3807 cyclic_splines_new_first_pt = {}
3808 for i in self.cyclic_splines:
3809 sp = self.main_curve.data.splines[i]
3811 for t in range(len(sp.bezier_points)):
3812 bp = sp.bezier_points[t]
3813 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
3814 cyclic_splines_new_first_pt[i] = t
3815 break # To take only one if there are more
3817 # Reorder
3818 for spline_idx in cyclic_splines_new_first_pt:
3819 sp = self.main_curve.data.splines[spline_idx]
3821 spline_old_coords = []
3822 for bp_old in sp.bezier_points:
3823 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
3825 left_handle_type = str(bp_old.handle_left_type)
3826 left_handle_length = float(bp_old.handle_left.length)
3827 left_handle_xyz = (
3828 float(bp_old.handle_left.x),
3829 float(bp_old.handle_left.y),
3830 float(bp_old.handle_left.z)
3832 right_handle_type = str(bp_old.handle_right_type)
3833 right_handle_length = float(bp_old.handle_right.length)
3834 right_handle_xyz = (
3835 float(bp_old.handle_right.x),
3836 float(bp_old.handle_right.y),
3837 float(bp_old.handle_right.z)
3839 spline_old_coords.append(
3840 [coords, left_handle_type,
3841 right_handle_type, left_handle_length,
3842 right_handle_length, left_handle_xyz,
3843 right_handle_xyz]
3846 for t in range(len(sp.bezier_points)):
3847 bp = sp.bezier_points
3849 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
3850 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
3851 else:
3852 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
3854 bp[t].co = Vector(spline_old_coords[new_index][0])
3856 bp[t].handle_left.length = spline_old_coords[new_index][3]
3857 bp[t].handle_right.length = spline_old_coords[new_index][4]
3859 bp[t].handle_left_type = "FREE"
3860 bp[t].handle_right_type = "FREE"
3862 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
3863 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
3864 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
3866 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
3867 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
3868 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
3870 bp[t].handle_left_type = spline_old_coords[new_index][1]
3871 bp[t].handle_right_type = spline_old_coords[new_index][2]
3873 # Invert the non-cyclic splines designated above
3874 for i in range(len(splines_to_invert)):
3875 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3877 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3878 self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
3879 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3881 bpy.ops.curve.switch_direction()
3883 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3885 # Keep selected the first vert of each spline
3886 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3887 for i in range(len(self.main_curve.data.splines)):
3888 if not self.main_curve.data.splines[i].use_cyclic_u:
3889 bp = self.main_curve.data.splines[i].bezier_points[0]
3890 else:
3891 bp = self.main_curve.data.splines[i].bezier_points[
3892 len(self.main_curve.data.splines[i].bezier_points) - 1
3895 bp.select_control_point = True
3896 bp.select_right_handle = True
3897 bp.select_left_handle = True
3899 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3901 return {'FINISHED'}
3903 def invoke(self, context, event):
3904 self.main_curve = bpy.context.object
3906 # Check if all curves are Bezier, and detect which ones are cyclic
3907 self.cyclic_splines = []
3908 for i in range(len(self.main_curve.data.splines)):
3909 if self.main_curve.data.splines[i].type != "BEZIER":
3910 self.report({'WARNING'}, "All splines must be Bezier type")
3912 return {'CANCELLED'}
3913 else:
3914 if self.main_curve.data.splines[i].use_cyclic_u:
3915 self.cyclic_splines.append(i)
3917 self.execute(context)
3918 self.report({'INFO'}, "First points have been set")
3920 return {'FINISHED'}
3923 # Add-ons Preferences Update Panel
3925 # Define Panel classes for updating
3926 panels = (
3927 VIEW3D_PT_tools_SURFSK_mesh,
3928 VIEW3D_PT_tools_SURFSK_curve
3932 def update_panel(self, context):
3933 message = "Bsurfaces GPL Edition: Updating Panel locations has failed"
3934 try:
3935 for panel in panels:
3936 if "bl_rna" in panel.__dict__:
3937 bpy.utils.unregister_class(panel)
3939 for panel in panels:
3940 panel.bl_category = context.preferences.addons[__name__].preferences.category
3941 bpy.utils.register_class(panel)
3943 except Exception as e:
3944 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
3945 pass
3947 def conver_gpencil_to_curve(context, pencil):
3948 newCurve = bpy.data.curves.new('gpencil_curve', type='CURVE') # curvedatablock
3949 newCurve.dimensions = '3D'
3950 CurveObject = object_utils.object_data_add(context, newCurve) # place in active scene
3952 for i, stroke in enumerate(pencil.data.layers[0].active_frame.strokes):
3953 stroke_points = pencil.data.layers[0].active_frame.strokes[i].points
3954 data_list = [ (point.co.x, point.co.y, point.co.z)
3955 for point in stroke_points ]
3956 points_to_add = len(data_list)-1
3958 flat_list = []
3959 for point in data_list:
3960 flat_list.extend(point)
3962 spline = newCurve.splines.new(type='BEZIER') # spline
3963 spline.bezier_points.add(points_to_add)
3964 spline.bezier_points.foreach_set("co", flat_list)
3966 for point in spline.bezier_points:
3967 point.handle_left_type="AUTO"
3968 point.handle_right_type="AUTO"
3970 return CurveObject
3972 class BsurfPreferences(AddonPreferences):
3973 # this must match the addon name, use '__package__'
3974 # when defining this in a submodule of a python package.
3975 bl_idname = __name__
3977 category: StringProperty(
3978 name="Tab Category",
3979 description="Choose a name for the category of the panel",
3980 default="Tools",
3981 update=update_panel
3984 def draw(self, context):
3985 layout = self.layout
3987 row = layout.row()
3988 col = row.column()
3989 col.label(text="Tab Category:")
3990 col.prop(self, "category", text="")
3993 # Properties
3994 class BsurfacesProps(PropertyGroup):
3995 SURFSK_cyclic_cross: BoolProperty(
3996 name="Cyclic Cross",
3997 description="Make cyclic the face-loops crossing the strokes",
3998 default=False
4000 SURFSK_cyclic_follow: BoolProperty(
4001 name="Cyclic Follow",
4002 description="Make cyclic the face-loops following the strokes",
4003 default=False
4005 SURFSK_keep_strokes: BoolProperty(
4006 name="Keep strokes",
4007 description="Keeps the sketched strokes or curves after adding the surface",
4008 default=False
4010 SURFSK_automatic_join: BoolProperty(
4011 name="Automatic join",
4012 description="Join automatically vertices of either surfaces "
4013 "generated by crosshatching, or from the borders of closed shapes",
4014 default=True
4016 SURFSK_loops_on_strokes: BoolProperty(
4017 name="Loops on strokes",
4018 description="Make the loops match the paths of the strokes",
4019 default=True
4021 SURFSK_precision: IntProperty(
4022 name="Precision",
4023 description="Precision level of the surface calculation",
4024 default=2,
4025 min=1,
4026 max=100
4028 SURFSK_object_with_retopology: PointerProperty(
4029 name="Retopology",
4030 type=bpy.types.Object
4032 SURFSK_object_with_strokes: PointerProperty(
4033 name="Strokes",
4034 type=bpy.types.Object
4037 classes = (
4038 GPENCIL_OT_SURFSK_add_surface,
4039 GPENCIL_OT_SURFSK_add_strokes,
4040 GPENCIL_OT_SURFSK_edit_strokes,
4041 CURVE_OT_SURFSK_reorder_splines,
4042 CURVE_OT_SURFSK_first_points,
4043 BsurfPreferences,
4044 BsurfacesProps,
4045 GPENCIL_OT_SURFSK_init
4048 def register():
4049 for cls in classes:
4050 bpy.utils.register_class(cls)
4052 bpy.types.Scene.bsurfaces = PointerProperty(type=BsurfacesProps)
4053 update_panel(None, bpy.context)
4055 def unregister():
4056 for cls in classes:
4057 bpy.utils.unregister_class(cls)
4059 del bpy.types.Scene.bsurfaces
4061 if __name__ == "__main__":
4062 register()