Update 3d-print toolbox to only export selection
[blender-addons.git] / mesh_bsurfaces.py
bloba6a6d52f64c7708718c427172f108f9030d951fb
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",
23 "version": (1, 5),
24 "blender": (2, 76, 0),
25 "location": "View3D > EditMode > ToolShelf",
26 "description": "Modeling and retopology tool.",
27 "wiki_url": "http://wiki.blender.org/index.php/Dev:Ref/Release_Notes/2.64/Bsurfaces_1.5",
28 "category": "Mesh",
32 import bpy
33 import bmesh
34 import math
35 import mathutils
36 import operator
38 from math import *
43 class VIEW3D_PT_tools_SURFSK_mesh(bpy.types.Panel):
44 bl_space_type = 'VIEW_3D'
45 bl_region_type = 'TOOLS'
46 bl_category = 'Tools'
47 bl_context = "mesh_edit"
48 bl_label = "Bsurfaces"
50 @classmethod
51 def poll(cls, context):
52 return context.active_object
55 def draw(self, context):
56 layout = self.layout
58 scn = context.scene
59 ob = context.object
61 col = layout.column(align=True)
62 row = layout.row()
63 row.separator()
64 col.operator("gpencil.surfsk_add_surface", text="Add Surface")
65 col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
66 col.prop(scn, "SURFSK_cyclic_cross")
67 col.prop(scn, "SURFSK_cyclic_follow")
68 col.prop(scn, "SURFSK_loops_on_strokes")
69 col.prop(scn, "SURFSK_automatic_join")
70 col.prop(scn, "SURFSK_keep_strokes")
74 class VIEW3D_PT_tools_SURFSK_curve(bpy.types.Panel):
75 bl_space_type = 'VIEW_3D'
76 bl_region_type = 'TOOLS'
77 bl_context = "curve_edit"
78 bl_category = 'Tools'
79 bl_label = "Bsurfaces"
81 @classmethod
82 def poll(cls, context):
83 return context.active_object
86 def draw(self, context):
87 layout = self.layout
89 scn = context.scene
90 ob = context.object
92 col = layout.column(align=True)
93 row = layout.row()
94 row.separator()
95 col.operator("curve.surfsk_first_points", text="Set First Points")
96 col.operator("curve.switch_direction", text="Switch Direction")
97 col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
102 #### Returns the type of strokes used.
103 def get_strokes_type(main_object):
104 strokes_type = ""
105 strokes_num = 0
107 # Check if they are grease pencil
108 try:
109 #### Get the active grease pencil layer.
110 strokes_num = len(main_object.grease_pencil.layers.active.active_frame.strokes)
112 if strokes_num > 0:
113 strokes_type = "GP_STROKES"
114 except:
115 pass
118 # Check if they are curves, if there aren't grease pencil strokes.
119 if strokes_type == "":
120 if len(bpy.context.selected_objects) == 2:
121 for ob in bpy.context.selected_objects:
122 if ob != bpy.context.scene.objects.active and ob.type == "CURVE":
123 strokes_type = "EXTERNAL_CURVE"
124 strokes_num = len(ob.data.splines)
126 # Check if there is any non-bezier spline.
127 for i in range(len(ob.data.splines)):
128 if ob.data.splines[i].type != "BEZIER":
129 strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
130 break
132 elif ob != bpy.context.scene.objects.active and ob.type != "CURVE":
133 strokes_type = "EXTERNAL_NO_CURVE"
134 elif len(bpy.context.selected_objects) > 2:
135 strokes_type = "MORE_THAN_ONE_EXTERNAL"
138 # Check if there is a single stroke without any selection in the object.
139 if strokes_num == 1 and main_object.data.total_vert_sel == 0:
140 if strokes_type == "EXTERNAL_CURVE":
141 strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
142 elif strokes_type == "GP_STROKES":
143 strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"
145 if strokes_num == 0 and main_object.data.total_vert_sel > 0:
146 strokes_type = "SELECTION_ALONE"
149 if strokes_type == "":
150 strokes_type = "NO_STROKES"
154 return strokes_type
159 # Surface generator operator.
160 class GPENCIL_OT_SURFSK_add_surface(bpy.types.Operator):
161 bl_idname = "gpencil.surfsk_add_surface"
162 bl_label = "Bsurfaces add surface"
163 bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
164 bl_options = {'REGISTER', 'UNDO'}
167 edges_U = bpy.props.IntProperty(name = "Cross",
168 description = "Number of face-loops crossing the strokes",
169 default = 1,
170 min = 1,
171 max = 200)
173 edges_V = bpy.props.IntProperty(name = "Follow",
174 description = "Number of face-loops following the strokes",
175 default = 1,
176 min = 1,
177 max = 200)
179 cyclic_cross = bpy.props.BoolProperty(name = "Cyclic Cross",
180 description = "Make cyclic the face-loops crossing the strokes",
181 default = False)
183 cyclic_follow = bpy.props.BoolProperty(name = "Cyclic Follow",
184 description = "Make cyclic the face-loops following the strokes",
185 default = False)
187 loops_on_strokes = bpy.props.BoolProperty(name = "Loops on strokes",
188 description = "Make the loops match the paths of the strokes",
189 default = False)
191 automatic_join = bpy.props.BoolProperty(name = "Automatic join",
192 description = "Join automatically vertices of either surfaces generated by crosshatching, or from the borders of closed shapes",
193 default = False)
195 join_stretch_factor = bpy.props.FloatProperty(name = "Stretch",
196 description = "Amount of stretching or shrinking allowed for edges when joining vertices automatically",
197 default = 1,
198 min = 0,
199 max = 3,
200 subtype = 'FACTOR')
205 def draw(self, context):
206 layout = self.layout
208 scn = context.scene
209 ob = context.object
211 col = layout.column(align=True)
212 row = layout.row()
214 if not self.is_fill_faces:
215 row.separator()
216 if not self.is_crosshatch:
217 if not self.selection_U_exists:
218 col.prop(self, "edges_U")
219 row.separator()
221 if not self.selection_V_exists:
222 col.prop(self, "edges_V")
223 row.separator()
225 row.separator()
227 if not self.selection_U_exists:
228 if not ((self.selection_V_exists and not self.selection_V_is_closed) or (self.selection_V2_exists and not self.selection_V2_is_closed)):
229 col.prop(self, "cyclic_cross")
231 if not self.selection_V_exists:
232 if not ((self.selection_U_exists and not self.selection_U_is_closed) or (self.selection_U2_exists and not self.selection_U2_is_closed)):
233 col.prop(self, "cyclic_follow")
236 col.prop(self, "loops_on_strokes")
238 col.prop(self, "automatic_join")
240 if self.automatic_join:
241 row.separator()
242 col.separator()
243 row.separator()
244 col.prop(self, "join_stretch_factor")
248 #### Get an ordered list of a chain of vertices.
249 def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx, first_vert_idx, middle_vertex_idx, closing_vert_idx):
250 # Order selected vertices.
251 verts_ordered = []
252 if closing_vert_idx != None:
253 verts_ordered.append(ob.data.vertices[closing_vert_idx])
255 verts_ordered.append(ob.data.vertices[first_vert_idx])
256 prev_v = first_vert_idx
257 prev_ed = None
258 finish_while = False
259 while True:
260 edges_non_matched = 0
261 for i in all_selected_edges_idx:
262 if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and ob.data.edges[i].vertices[1] in all_selected_verts_idx:
263 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
264 prev_v = ob.data.edges[i].vertices[1]
265 prev_ed = ob.data.edges[i]
266 elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and ob.data.edges[i].vertices[0] in all_selected_verts_idx:
267 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
268 prev_v = ob.data.edges[i].vertices[0]
269 prev_ed = ob.data.edges[i]
270 else:
271 edges_non_matched += 1
273 if edges_non_matched == len(all_selected_edges_idx):
274 finish_while = True
276 if finish_while:
277 break
279 if closing_vert_idx != None:
280 verts_ordered.append(ob.data.vertices[closing_vert_idx])
282 if middle_vertex_idx != None:
283 verts_ordered.append(ob.data.vertices[middle_vertex_idx])
284 verts_ordered.reverse()
286 return tuple(verts_ordered)
289 #### Calculates length of a chain of points.
290 def get_chain_length(self, object, verts_ordered):
291 matrix = object.matrix_world
293 edges_lengths = []
294 edges_lengths_sum = 0
295 for i in range(0, len(verts_ordered)):
296 if i == 0:
297 prev_v_co = matrix * verts_ordered[i].co
298 else:
299 v_co = matrix * verts_ordered[i].co
301 v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
302 edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
304 edges_lengths.append(edge_length)
305 edges_lengths_sum += edge_length
307 prev_v_co = v_co
309 return edges_lengths, edges_lengths_sum
312 #### Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
313 def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
314 edges_proportions = []
315 if use_boundaries:
316 verts_count = 1
317 for l in edges_lengths:
318 edges_proportions.append(l / edges_lengths_sum)
319 verts_count += 1
320 else:
321 verts_count = 1
322 for n in range(0, fixed_edges_num):
323 edges_proportions.append(1 / fixed_edges_num)
324 verts_count += 1
326 return edges_proportions
329 #### Calculates the angle between two pairs of points in space.
330 def orientation_difference(self, points_A_co, points_B_co): # each parameter should be a list with two elements, and each element should be a x,y,z coordinate.
331 vec_A = points_A_co[0] - points_A_co[1]
332 vec_B = points_B_co[0] - points_B_co[1]
334 angle = vec_A.angle(vec_B)
336 if angle > 0.5 * math.pi:
337 angle = abs(angle - math.pi)
339 return angle
343 #### Calculate the which vert of verts_idx list is the nearest one to the point_co coordinates, and the distance.
344 def shortest_distance(self, object, point_co, verts_idx):
345 matrix = object.matrix_world
347 for i in range(0, len(verts_idx)):
348 dist = (point_co - matrix * object.data.vertices[verts_idx[i]].co).length
349 if i == 0:
350 prev_dist = dist
351 nearest_vert_idx = verts_idx[i]
352 shortest_dist = dist
354 if dist < prev_dist:
355 prev_dist = dist
356 nearest_vert_idx = verts_idx[i]
357 shortest_dist = dist
359 return nearest_vert_idx, shortest_dist
362 #### Returns the index of the opposite vert tip in a chain, given a vert tip index as parameter, and a multidimentional list with all pairs of tips.
363 def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
364 opposite_vert_tip_idx = None
365 for i in range(0, len(all_chains_tips_idx)):
366 if vert_tip_idx == all_chains_tips_idx[i][0]:
367 opposite_vert_tip_idx = all_chains_tips_idx[i][1]
368 if vert_tip_idx == all_chains_tips_idx[i][1]:
369 opposite_vert_tip_idx = all_chains_tips_idx[i][0]
371 return opposite_vert_tip_idx
375 #### Simplifies a spline and returns the new points coordinates.
376 def simplify_spline(self, spline_coords, segments_num):
377 simplified_spline = []
378 points_between_segments = round(len(spline_coords) / segments_num)
380 simplified_spline.append(spline_coords[0])
381 for i in range(1, segments_num):
382 simplified_spline.append(spline_coords[i * points_between_segments])
384 simplified_spline.append(spline_coords[len(spline_coords) - 1])
386 return simplified_spline
390 #### Cleans up the scene and gets it the same it was at the beginning, in case the script is interrupted in the middle of the execution.
391 def cleanup_on_interruption(self):
392 # If the original strokes curve comes from conversion from grease pencil and wasn't made by hand, delete it.
393 if not self.using_external_curves:
394 try:
395 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
396 bpy.data.objects[self.original_curve.name].select = True
397 bpy.context.scene.objects.active = bpy.data.objects[self.original_curve.name]
399 bpy.ops.object.delete()
400 except:
401 pass
403 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
404 bpy.data.objects[self.main_object.name].select = True
405 bpy.context.scene.objects.active = bpy.data.objects[self.main_object.name]
406 else:
407 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
408 bpy.data.objects[self.original_curve.name].select = True
409 bpy.data.objects[self.main_object.name].select = True
410 bpy.context.scene.objects.active = bpy.data.objects[self.main_object.name]
412 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
416 #### Returns a list with the coords of the points distributed over the splines passed to this method according to the proportions parameter.
417 def distribute_pts(self, surface_splines, proportions):
418 # Calculate the length of each final surface spline.
419 surface_splines_lengths = []
420 surface_splines_parsed = []
421 for sp_idx in range(0, len(surface_splines)):
422 # Calculate spline length
423 surface_splines_lengths.append(0)
424 for i in range(0, len(surface_splines[sp_idx].bezier_points)):
425 if i == 0:
426 prev_p = surface_splines[sp_idx].bezier_points[i]
427 else:
428 p = surface_splines[sp_idx].bezier_points[i]
430 edge_length = (prev_p.co - p.co).length
432 surface_splines_lengths[sp_idx] += edge_length
434 prev_p = p
437 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline.
438 for sp_idx in range(0, len(surface_splines)):
439 surface_splines_parsed.append([])
440 surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
442 prev_p_co = surface_splines[sp_idx].bezier_points[0].co
443 p_idx = 0
444 for prop_idx in range(len(proportions) - 1):
445 target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
447 partial_segment_length = 0
450 finish_while = False
451 while True:
452 p_co = surface_splines[sp_idx].bezier_points[p_idx].co
454 new_dist = (prev_p_co - p_co).length
456 potential_segment_length = partial_segment_length + new_dist # The new distance that could have the partial segment if it is still shorter than the target length.
459 if potential_segment_length < target_length: # If the potential is still shorter, keep adding.
460 partial_segment_length = potential_segment_length
462 p_idx += 1
463 prev_p_co = p_co
465 elif potential_segment_length > target_length: # If the potential is longer than the target, calculate the target (a point between the last two points), and assign.
466 remaining_dist = target_length - partial_segment_length
467 vec = p_co - prev_p_co
468 vec.normalize()
469 intermediate_co = prev_p_co + (vec * remaining_dist)
471 surface_splines_parsed[sp_idx].append(intermediate_co)
473 partial_segment_length += remaining_dist
474 prev_p_co = intermediate_co
476 finish_while = True
478 elif potential_segment_length == target_length: # If the potential is equal to the target, assign.
479 surface_splines_parsed[sp_idx].append(p_co)
481 prev_p_co = p_co
483 finish_while = True
485 if finish_while:
486 break
488 # last point of the spline
489 surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co)
492 return surface_splines_parsed
496 #### Counts the number of faces that belong to each edge.
497 def edge_face_count(self, ob):
498 ed_keys_count_dict = {}
500 for face in ob.data.polygons:
501 for ed_keys in face.edge_keys:
502 if not ed_keys in ed_keys_count_dict:
503 ed_keys_count_dict[ed_keys] = 1
504 else:
505 ed_keys_count_dict[ed_keys] += 1
508 edge_face_count = []
509 for i in range(len(ob.data.edges)):
510 edge_face_count.append(0)
512 for i in range(len(ob.data.edges)):
513 ed = ob.data.edges[i]
515 v1 = ed.vertices[0]
516 v2 = ed.vertices[1]
518 if (v1, v2) in ed_keys_count_dict:
519 edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
520 elif (v2, v1) in ed_keys_count_dict:
521 edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
524 return edge_face_count
528 #### Fills with faces all the selected vertices which form empty triangles or quads.
529 def fill_with_faces(self, object):
530 all_selected_verts_count = self.main_object_selected_verts_count
533 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
535 #### Calculate average length of selected edges.
536 all_selected_verts = []
537 original_sel_edges_count = 0
538 for ed in object.data.edges:
539 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
540 coords = []
541 coords.append(object.data.vertices[ed.vertices[0]].co)
542 coords.append(object.data.vertices[ed.vertices[1]].co)
544 original_sel_edges_count += 1
546 if not ed.vertices[0] in all_selected_verts:
547 all_selected_verts.append(ed.vertices[0])
549 if not ed.vertices[1] in all_selected_verts:
550 all_selected_verts.append(ed.vertices[1])
553 tuple(all_selected_verts)
556 #### Check if there is any edge selected. If not, interrupt the script.
557 if original_sel_edges_count == 0 and all_selected_verts_count > 0:
558 return 0
562 #### Get all edges connected to selected verts.
563 all_edges_around_sel_verts = []
564 edges_connected_to_sel_verts = {}
565 verts_connected_to_every_vert = {}
566 for ed_idx in range(len(object.data.edges)):
567 ed = object.data.edges[ed_idx]
568 include_edge = False
570 if ed.vertices[0] in all_selected_verts:
571 if not ed.vertices[0] in edges_connected_to_sel_verts:
572 edges_connected_to_sel_verts[ed.vertices[0]] = []
574 edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
575 include_edge = True
577 if ed.vertices[1] in all_selected_verts:
578 if not ed.vertices[1] in edges_connected_to_sel_verts:
579 edges_connected_to_sel_verts[ed.vertices[1]] = []
581 edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
582 include_edge = True
585 if include_edge == True:
586 all_edges_around_sel_verts.append(ed_idx)
589 # Get all connected verts to each vert.
590 if not ed.vertices[0] in verts_connected_to_every_vert:
591 verts_connected_to_every_vert[ed.vertices[0]] = []
593 if not ed.vertices[1] in verts_connected_to_every_vert:
594 verts_connected_to_every_vert[ed.vertices[1]] = []
596 verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
597 verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
602 #### Get all verts connected to faces.
603 all_verts_part_of_faces = []
604 all_edges_faces_count = []
605 all_edges_faces_count += self.edge_face_count(object)
607 # Get only the selected edges that have faces attached.
608 count_faces_of_edges_around_sel_verts = {}
609 selected_verts_with_faces = []
610 for ed_idx in all_edges_around_sel_verts:
611 count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
613 if all_edges_faces_count[ed_idx] > 0:
614 ed = object.data.edges[ed_idx]
616 if not ed.vertices[0] in selected_verts_with_faces:
617 selected_verts_with_faces.append(ed.vertices[0])
619 if not ed.vertices[1] in selected_verts_with_faces:
620 selected_verts_with_faces.append(ed.vertices[1])
622 all_verts_part_of_faces.append(ed.vertices[0])
623 all_verts_part_of_faces.append(ed.vertices[1])
625 tuple(selected_verts_with_faces)
629 #### Discard unneeded verts from calculations.
630 participating_verts = []
631 movable_verts = []
632 for v_idx in all_selected_verts:
633 vert_has_edges_with_one_face = False
635 for ed_idx in edges_connected_to_sel_verts[v_idx]: # Check if the actual vert has at least one edge connected to only one face.
636 if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
637 vert_has_edges_with_one_face = True
639 # If the vert has two or less edges connected and the vert is not part of any face. Or the vert is part of any face and at least one of the connected edges has only one face attached to it.
640 if (len(edges_connected_to_sel_verts[v_idx]) == 2 and not v_idx in all_verts_part_of_faces) or len(edges_connected_to_sel_verts[v_idx]) == 1 or (v_idx in all_verts_part_of_faces and vert_has_edges_with_one_face):
641 participating_verts.append(v_idx)
643 if not v_idx in all_verts_part_of_faces:
644 movable_verts.append(v_idx)
648 #### Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
649 for mv_idx in movable_verts:
650 freeze_vert = False
651 mv_connected_verts = verts_connected_to_every_vert[mv_idx]
653 for actual_v_idx in all_selected_verts:
654 count_shared_neighbors = 0
655 checked_verts = []
657 for mv_conn_v_idx in mv_connected_verts:
658 if mv_idx != actual_v_idx:
659 if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and not mv_conn_v_idx in checked_verts:
660 count_shared_neighbors += 1
661 checked_verts.append(mv_conn_v_idx)
664 if actual_v_idx in mv_connected_verts:
665 freeze_vert = True
666 break
668 if count_shared_neighbors == 2:
669 freeze_vert = True
670 break
672 if freeze_vert:
673 break
675 if freeze_vert:
676 movable_verts.remove(mv_idx)
680 #### Calculate merge distance for participating verts.
681 shortest_edge_length = None
682 for ed in object.data.edges:
683 if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
684 v1 = object.data.vertices[ed.vertices[0]]
685 v2 = object.data.vertices[ed.vertices[1]]
687 length = (v1.co - v2.co).length
689 if shortest_edge_length == None:
690 shortest_edge_length = length
691 else:
692 if length < shortest_edge_length:
693 shortest_edge_length = length
695 if shortest_edge_length != None:
696 edges_merge_distance = shortest_edge_length * 0.5
697 else:
698 edges_merge_distance = 0
703 #### Get together the verts near enough. They will be merged later.
704 remaining_verts = []
705 remaining_verts += participating_verts
706 for v1_idx in participating_verts:
707 if v1_idx in remaining_verts and v1_idx in movable_verts:
708 verts_to_merge = []
709 coords_verts_to_merge = {}
711 verts_to_merge.append(v1_idx)
713 v1_co = object.data.vertices[v1_idx].co
714 coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
717 for v2_idx in remaining_verts:
718 if v1_idx != v2_idx:
719 v2_co = object.data.vertices[v2_idx].co
721 dist = (v1_co - v2_co).length
723 if dist <= edges_merge_distance: # Add the verts which are near enough.
724 verts_to_merge.append(v2_idx)
726 coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
729 for vm_idx in verts_to_merge:
730 remaining_verts.remove(vm_idx)
733 if len(verts_to_merge) > 1:
734 # Calculate middle point of the verts to merge.
735 sum_x_co = 0
736 sum_y_co = 0
737 sum_z_co = 0
738 movable_verts_to_merge_count = 0
739 for i in range(len(verts_to_merge)):
740 if verts_to_merge[i] in movable_verts:
741 v_co = object.data.vertices[verts_to_merge[i]].co
743 sum_x_co += v_co[0]
744 sum_y_co += v_co[1]
745 sum_z_co += v_co[2]
747 movable_verts_to_merge_count += 1
749 middle_point_co = [sum_x_co / movable_verts_to_merge_count, sum_y_co / movable_verts_to_merge_count, sum_z_co / movable_verts_to_merge_count]
752 # Check if any vert to be merged is not movable.
753 shortest_dist = None
754 are_verts_not_movable = False
755 verts_not_movable = []
756 for v_merge_idx in verts_to_merge:
757 if v_merge_idx in participating_verts and not v_merge_idx in movable_verts:
758 are_verts_not_movable = True
759 verts_not_movable.append(v_merge_idx)
761 if are_verts_not_movable:
762 # Get the vert connected to faces, that is nearest to the middle point of the movable verts.
763 shortest_dist = None
764 for vcf_idx in verts_not_movable:
765 dist = abs((object.data.vertices[vcf_idx].co - mathutils.Vector(middle_point_co)).length)
767 if shortest_dist == None:
768 shortest_dist = dist
769 nearest_vert_idx = vcf_idx
770 else:
771 if dist < shortest_dist:
772 shortest_dist = dist
773 nearest_vert_idx = vcf_idx
775 coords = object.data.vertices[nearest_vert_idx].co
776 target_point_co = [coords[0], coords[1], coords[2]]
777 else:
778 target_point_co = middle_point_co
781 # Move verts to merge to the middle position.
782 for v_merge_idx in verts_to_merge:
783 if v_merge_idx in movable_verts: # Only move the verts that are not part of faces.
784 object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
785 object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
786 object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
790 #### Perform "Remove Doubles" to weld all the disconnected verts
791 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
792 bpy.ops.mesh.remove_doubles(threshold=0.0001)
794 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
797 #### Get all the definitive selected edges, after weldding.
798 selected_edges = []
799 edges_per_vert = {} # Number of faces of each selected edge.
800 for ed in object.data.edges:
801 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
802 selected_edges.append(ed.index)
804 # Save all the edges that belong to each vertex.
805 if not ed.vertices[0] in edges_per_vert:
806 edges_per_vert[ed.vertices[0]] = []
808 if not ed.vertices[1] in edges_per_vert:
809 edges_per_vert[ed.vertices[1]] = []
811 edges_per_vert[ed.vertices[0]].append(ed.index)
812 edges_per_vert[ed.vertices[1]].append(ed.index)
814 # Check if all the edges connected to each vert have two faces attached to them. To discard them later and make calculations faster.
815 a = []
816 a += self.edge_face_count(object)
817 tuple(a)
818 verts_surrounded_by_faces = {}
819 for v_idx in edges_per_vert:
820 edges = edges_per_vert[v_idx]
822 edges_with_two_faces_count = 0
823 for ed_idx in edges_per_vert[v_idx]:
824 if a[ed_idx] == 2:
825 edges_with_two_faces_count += 1
827 if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
828 verts_surrounded_by_faces[v_idx] = True
829 else:
830 verts_surrounded_by_faces[v_idx] = False
833 #### Get all the selected vertices.
834 selected_verts_idx = []
835 for v in object.data.vertices:
836 if v.select:
837 selected_verts_idx.append(v.index)
840 #### Get all the faces of the object.
841 all_object_faces_verts_idx = []
842 for face in object.data.polygons:
843 face_verts = []
844 face_verts.append(face.vertices[0])
845 face_verts.append(face.vertices[1])
846 face_verts.append(face.vertices[2])
848 if len(face.vertices) == 4:
849 face_verts.append(face.vertices[3])
851 all_object_faces_verts_idx.append(face_verts)
854 #### Deselect all vertices.
855 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
856 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
857 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
861 #### Make a dictionary with the verts related to each vert.
862 related_key_verts = {}
863 for ed_idx in selected_edges:
864 ed = object.data.edges[ed_idx]
866 if not verts_surrounded_by_faces[ed.vertices[0]]:
867 if not ed.vertices[0] in related_key_verts:
868 related_key_verts[ed.vertices[0]] = []
870 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
871 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
873 if not verts_surrounded_by_faces[ed.vertices[1]]:
874 if not ed.vertices[1] in related_key_verts:
875 related_key_verts[ed.vertices[1]] = []
877 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
878 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
882 #### Get groups of verts forming each face.
883 faces_verts_idx = []
884 for v1 in related_key_verts: # verts-1 ....
885 for v2 in related_key_verts: # verts-2
886 if v1 != v2:
887 related_verts_in_common = []
888 v2_in_rel_v1 = False
889 v1_in_rel_v2 = False
890 for rel_v1 in related_key_verts[v1]:
891 if rel_v1 in related_key_verts[v2]: # Check if related verts of verts-1 are related verts of verts-2.
892 related_verts_in_common.append(rel_v1)
894 if v2 in related_key_verts[v1]:
895 v2_in_rel_v1 = True
897 if v1 in related_key_verts[v2]:
898 v1_in_rel_v2 = True
901 repeated_face = False
902 # If two verts have two related verts in common, they form a quad.
903 if len(related_verts_in_common) == 2:
904 # Check if the face is already saved.
905 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
908 for f_verts in all_faces_to_check_idx:
909 repeated_verts = 0
911 if len(f_verts) == 4:
912 if v1 in f_verts: repeated_verts += 1
913 if v2 in f_verts: repeated_verts += 1
914 if related_verts_in_common[0] in f_verts: repeated_verts += 1
915 if related_verts_in_common[1] in f_verts: repeated_verts += 1
917 if repeated_verts == len(f_verts):
918 repeated_face = True
919 break
921 if not repeated_face:
922 faces_verts_idx.append([v1, related_verts_in_common[0], v2, related_verts_in_common[1]])
924 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1: # If Two verts have one related vert in common and they are related to each other, they form a triangle.
925 # Check if the face is already saved.
926 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
928 for f_verts in all_faces_to_check_idx:
929 repeated_verts = 0
931 if len(f_verts) == 3:
932 if v1 in f_verts: repeated_verts += 1
933 if v2 in f_verts: repeated_verts += 1
934 if related_verts_in_common[0] in f_verts: repeated_verts += 1
936 if repeated_verts == len(f_verts):
937 repeated_face = True
938 break
940 if not repeated_face:
941 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
944 #### Keep only the faces that don't overlap by ignoring quads that overlap with two adjacent triangles.
945 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate.
946 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
947 for i in range(len(faces_verts_idx)):
948 for t in range(len(all_faces_to_check_idx)):
949 if i != t:
950 verts_in_common = 0
952 if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
953 for v_idx in all_faces_to_check_idx[t]:
954 if v_idx in faces_verts_idx[i]:
955 verts_in_common += 1
957 if verts_in_common == 3: # If it doesn't have all it's vertices repeated in the other face.
958 if not i in faces_to_not_include_idx:
959 faces_to_not_include_idx.append(i)
962 #### Build faces discarding the ones in faces_to_not_include.
963 me = object.data
964 bm = bmesh.new()
965 bm.from_mesh(me)
967 num_faces_created = 0
968 for i in range(len(faces_verts_idx)):
969 if not i in faces_to_not_include_idx:
970 bm.faces.new([ bm.verts[v] for v in faces_verts_idx[i] ])
972 num_faces_created += 1
974 bm.to_mesh(me)
975 bm.free()
979 for v_idx in selected_verts_idx:
980 self.main_object.data.vertices[v_idx].select = True
983 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
984 bpy.ops.mesh.normals_make_consistent(inside=False)
985 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
988 return num_faces_created
992 #### Crosshatch skinning.
993 def crosshatch_surface_invoke(self, ob_original_splines):
994 self.is_crosshatch = False
995 self.crosshatch_merge_distance = 0
998 objects_to_delete = [] # duplicated strokes to be deleted.
1000 # If the main object uses modifiers deactivate them temporarily until the surface is joined. (without this the surface verts merging with the main object doesn't work well)
1001 self.modifiers_prev_viewport_state = []
1002 if len(self.main_object.modifiers) > 0:
1003 for m_idx in range(len(self.main_object.modifiers)):
1004 self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
1006 self.main_object.modifiers[m_idx].show_viewport = False
1009 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1010 bpy.data.objects[ob_original_splines.name].select = True
1011 bpy.context.scene.objects.active = bpy.data.objects[ob_original_splines.name]
1014 if len(ob_original_splines.data.splines) >= 2:
1015 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1016 ob_splines = bpy.context.object
1017 ob_splines.name = "SURFSKIO_NE_STR"
1020 #### Get estimative merge distance (sum up the distances from the first point to all other points, then average them and then divide them).
1021 first_point_dist_sum = 0
1022 first_dist = 0
1023 second_dist = 0
1024 coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
1025 for i in range(len(ob_splines.data.splines)):
1026 sp = ob_splines.data.splines[i]
1028 if coords_first_pt != sp.bezier_points[0].co:
1029 first_dist = (coords_first_pt - sp.bezier_points[0].co).length
1031 if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
1032 second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
1034 first_point_dist_sum += first_dist + second_dist
1037 if i == 0:
1038 if first_dist != 0:
1039 shortest_dist = first_dist
1040 elif second_dist != 0:
1041 shortest_dist = second_dist
1044 if shortest_dist > first_dist and first_dist != 0:
1045 shortest_dist = first_dist
1047 if shortest_dist > second_dist and second_dist != 0:
1048 shortest_dist = second_dist
1051 self.crosshatch_merge_distance = shortest_dist / 20
1055 #### Recalculation of merge distance.
1057 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1059 ob_calc_merge_dist = bpy.context.object
1060 ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
1062 objects_to_delete.append(ob_calc_merge_dist)
1066 #### Smooth out strokes a little to improve crosshatch detection.
1067 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1068 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
1070 for i in range(4):
1071 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1073 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1074 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1078 #### Convert curves into mesh.
1079 ob_calc_merge_dist.data.resolution_u = 12
1080 bpy.ops.object.convert(target='MESH', keep_original=False)
1082 # Find "intersection-nodes".
1083 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1084 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1085 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=self.crosshatch_merge_distance)
1086 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1087 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1089 # Remove verts with less than three edges.
1090 verts_edges_count = {}
1091 for ed in ob_calc_merge_dist.data.edges:
1092 v = ed.vertices
1094 if v[0] not in verts_edges_count:
1095 verts_edges_count[v[0]] = 0
1097 if v[1] not in verts_edges_count:
1098 verts_edges_count[v[1]] = 0
1100 verts_edges_count[v[0]] += 1
1101 verts_edges_count[v[1]] += 1
1103 nodes_verts_coords = []
1104 for v_idx in verts_edges_count:
1105 v = ob_calc_merge_dist.data.vertices[v_idx]
1107 if verts_edges_count[v_idx] < 3:
1108 v.select = True
1111 # Remove them.
1112 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1113 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
1114 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1116 # Remove doubles to discard very near verts from calculations of distance.
1117 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=self.crosshatch_merge_distance * 4.0)
1118 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1120 # Get all coords of the resulting nodes.
1121 nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for v in ob_calc_merge_dist.data.vertices]
1123 #### Check if the strokes are a crosshatch.
1124 if len(nodes_verts_coords) >= 3:
1125 self.is_crosshatch = True
1127 shortest_dist = None
1128 for co_1 in nodes_verts_coords:
1129 for co_2 in nodes_verts_coords:
1130 if co_1 != co_2:
1131 dist = (mathutils.Vector(co_1) - mathutils.Vector(co_2)).length
1133 if shortest_dist != None:
1134 if dist < shortest_dist:
1135 shortest_dist = dist
1136 else:
1137 shortest_dist = dist
1139 self.crosshatch_merge_distance = shortest_dist / 3
1142 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1143 bpy.data.objects[ob_splines.name].select = True
1144 bpy.context.scene.objects.active = bpy.data.objects[ob_splines.name]
1146 #### Deselect all points.
1147 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1148 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1149 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1153 #### Smooth splines in a localized way, to eliminate "saw-teeth" like shapes when there are many points.
1154 for sp in ob_splines.data.splines:
1155 angle_sum = 0
1157 angle_limit = 2 # Degrees
1158 for t in range(len(sp.bezier_points)):
1159 if t <= len(sp.bezier_points) - 3: # Because on each iteration it checks the "next two points" of the actual. This way it doesn't go out of range.
1160 p1 = sp.bezier_points[t]
1161 p2 = sp.bezier_points[t + 1]
1162 p3 = sp.bezier_points[t + 2]
1164 vec_1 = p1.co - p2.co
1165 vec_2 = p2.co - p3.co
1167 if p2.co != p1.co and p2.co != p3.co:
1168 angle = vec_1.angle(vec_2)
1169 angle_sum += degrees(angle)
1171 if angle_sum >= angle_limit: # If sum of angles is grater than the limit.
1172 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1173 p1.select_control_point = True; p1.select_left_handle = True; p1.select_right_handle = True
1174 p2.select_control_point = True; p2.select_left_handle = True; p2.select_right_handle = True
1176 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1177 p3.select_control_point = True; p3.select_left_handle = True; p3.select_right_handle = True
1179 angle_sum = 0
1181 sp.bezier_points[0].select_control_point = False
1182 sp.bezier_points[0].select_left_handle = False
1183 sp.bezier_points[0].select_right_handle = False
1185 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
1186 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
1187 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
1191 #### Smooth out strokes a little to improve crosshatch detection.
1192 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1194 for i in range(15):
1195 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1197 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1198 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1203 #### Simplify the splines.
1204 for sp in ob_splines.data.splines:
1205 angle_sum = 0
1207 sp.bezier_points[0].select_control_point = True
1208 sp.bezier_points[0].select_left_handle = True
1209 sp.bezier_points[0].select_right_handle = True
1211 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
1212 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
1213 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
1216 angle_limit = 15 # Degrees
1217 for t in range(len(sp.bezier_points)):
1218 if t <= len(sp.bezier_points) - 3: # Because on each iteration it checks the "next two points" of the actual. This way it doesn't go out of range.
1219 p1 = sp.bezier_points[t]
1220 p2 = sp.bezier_points[t + 1]
1221 p3 = sp.bezier_points[t + 2]
1223 vec_1 = p1.co - p2.co
1224 vec_2 = p2.co - p3.co
1226 if p2.co != p1.co and p2.co != p3.co:
1227 angle = vec_1.angle(vec_2)
1228 angle_sum += degrees(angle)
1230 if angle_sum >= angle_limit: # If sum of angles is grater than the limit.
1231 p1.select_control_point = True; p1.select_left_handle = True; p1.select_right_handle = True
1232 p2.select_control_point = True; p2.select_left_handle = True; p2.select_right_handle = True
1233 p3.select_control_point = True; p3.select_left_handle = True; p3.select_right_handle = True
1235 angle_sum = 0
1239 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1241 bpy.ops.curve.select_all(action = 'INVERT')
1243 bpy.ops.curve.delete(type='VERT')
1244 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1248 objects_to_delete.append(ob_splines)
1251 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1252 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1253 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1256 #### Check if the strokes are a crosshatch.
1257 if self.is_crosshatch:
1258 all_points_coords = []
1259 for i in range(len(ob_splines.data.splines)):
1260 all_points_coords.append([])
1262 all_points_coords[i] = [mathutils.Vector((x, y, z)) for x, y, z in [bp.co for bp in ob_splines.data.splines[i].bezier_points]]
1265 all_intersections = []
1266 checked_splines = []
1267 for i in range(len(all_points_coords)):
1269 for t in range(len(all_points_coords[i]) - 1):
1270 bp1_co = all_points_coords[i][t]
1271 bp2_co = all_points_coords[i][t + 1]
1273 for i2 in range(len(all_points_coords)):
1274 if i != i2 and not i2 in checked_splines:
1275 for t2 in range(len(all_points_coords[i2]) - 1):
1276 bp3_co = all_points_coords[i2][t2]
1277 bp4_co = all_points_coords[i2][t2 + 1]
1280 intersec_coords = mathutils.geometry.intersect_line_line(bp1_co, bp2_co, bp3_co, bp4_co)
1282 if intersec_coords != None:
1283 dist = (intersec_coords[0] - intersec_coords[1]).length
1285 if dist <= self.crosshatch_merge_distance * 1.5:
1286 temp_co, percent1 = mathutils.geometry.intersect_point_line(intersec_coords[0], bp1_co, bp2_co)
1288 if (percent1 >= -0.02 and percent1 <= 1.02):
1289 temp_co, percent2 = mathutils.geometry.intersect_point_line(intersec_coords[1], bp3_co, bp4_co)
1290 if (percent2 >= -0.02 and percent2 <= 1.02):
1291 all_intersections.append((i, t, percent1, ob_splines.matrix_world * intersec_coords[0])) # Format: spline index, first point index from corresponding segment, percentage from first point of actual segment, coords of intersection point.
1292 all_intersections.append((i2, t2, percent2, ob_splines.matrix_world * intersec_coords[1]))
1296 checked_splines.append(i)
1299 all_intersections.sort(key = operator.itemgetter(0,1,2)) # Sort list by spline, then by corresponding first point index of segment, and then by percentage from first point of segment: elements 0 and 1 respectively.
1303 self.crosshatch_strokes_coords = {}
1304 for i in range(len(all_intersections)):
1305 if not all_intersections[i][0] in self.crosshatch_strokes_coords:
1306 self.crosshatch_strokes_coords[all_intersections[i][0]] = []
1308 self.crosshatch_strokes_coords[all_intersections[i][0]].append(all_intersections[i][3]) # Save intersection coords.
1310 else:
1311 self.is_crosshatch = False
1314 #### Delete all duplicates.
1315 for o in objects_to_delete:
1316 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1317 bpy.data.objects[o.name].select = True
1318 bpy.context.scene.objects.active = bpy.data.objects[o.name]
1319 bpy.ops.object.delete()
1322 #### If the main object has modifiers, turn their "viewport view status" to what it was before the forced deactivation above.
1323 if len(self.main_object.modifiers) > 0:
1324 for m_idx in range(len(self.main_object.modifiers)):
1325 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1329 return
1333 #### Part of the Crosshatch process that is repeated when the operator is tweaked.
1334 def crosshatch_surface_execute(self):
1335 # If the main object uses modifiers deactivate them temporarily until the surface is joined. (without this the surface verts merging with the main object doesn't work well)
1336 self.modifiers_prev_viewport_state = []
1337 if len(self.main_object.modifiers) > 0:
1338 for m_idx in range(len(self.main_object.modifiers)):
1339 self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
1341 self.main_object.modifiers[m_idx].show_viewport = False
1344 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1348 me_name = "SURFSKIO_STK_TMP"
1349 me = bpy.data.meshes.new(me_name)
1351 all_verts_coords = []
1352 all_edges = []
1353 for st_idx in self.crosshatch_strokes_coords:
1354 for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
1355 coords = self.crosshatch_strokes_coords[st_idx][co_idx]
1357 all_verts_coords.append(coords)
1359 if co_idx > 0:
1360 all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
1363 me.from_pydata(all_verts_coords, all_edges, [])
1365 me.update()
1367 ob = bpy.data.objects.new(me_name, me)
1368 ob.data = me
1369 bpy.context.scene.objects.link(ob)
1372 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1373 bpy.data.objects[ob.name].select = True
1374 bpy.context.scene.objects.active = bpy.data.objects[ob.name]
1377 #### Get together each vert and its nearest, to the middle position.
1378 verts = ob.data.vertices
1379 checked_verts = []
1380 for i in range(len(verts)):
1381 shortest_dist = None
1383 if not i in checked_verts:
1384 for t in range(len(verts)):
1385 if i != t and not t in checked_verts:
1386 dist = (verts[i].co - verts[t].co).length
1388 if shortest_dist != None:
1389 if dist < shortest_dist:
1390 shortest_dist = dist
1391 nearest_vert = t
1392 else:
1393 shortest_dist = dist
1394 nearest_vert = t
1396 middle_location = (verts[i].co + verts[nearest_vert].co) / 2
1398 verts[i].co = middle_location
1399 verts[nearest_vert].co = middle_location
1401 checked_verts.append(i)
1402 checked_verts.append(nearest_vert)
1407 #### Calculate average length between all the generated edges.
1408 ob = bpy.context.object
1409 lengths_sum = 0
1410 for ed in ob.data.edges:
1411 v1 = ob.data.vertices[ed.vertices[0]]
1412 v2 = ob.data.vertices[ed.vertices[1]]
1414 lengths_sum += (v1.co - v2.co).length
1416 edges_count = len(ob.data.edges)
1418 average_edge_length = lengths_sum / edges_count
1421 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1422 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1423 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=average_edge_length / 15.0)
1424 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1426 final_points_ob = bpy.context.scene.objects.active
1429 #### Make a dictionary with the verts related to each vert.
1430 related_key_verts = {}
1431 for ed in final_points_ob.data.edges:
1432 if not ed.vertices[0] in related_key_verts:
1433 related_key_verts[ed.vertices[0]] = []
1435 if not ed.vertices[1] in related_key_verts:
1436 related_key_verts[ed.vertices[1]] = []
1439 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
1440 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
1442 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
1443 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
1447 #### Get groups of verts forming each face.
1448 faces_verts_idx = []
1449 for v1 in related_key_verts: # verts-1 ....
1450 for v2 in related_key_verts: # verts-2
1451 if v1 != v2:
1452 related_verts_in_common = []
1453 v2_in_rel_v1 = False
1454 v1_in_rel_v2 = False
1455 for rel_v1 in related_key_verts[v1]:
1456 if rel_v1 in related_key_verts[v2]: # Check if related verts of verts-1 are related verts of verts-2.
1457 related_verts_in_common.append(rel_v1)
1459 if v2 in related_key_verts[v1]:
1460 v2_in_rel_v1 = True
1462 if v1 in related_key_verts[v2]:
1463 v1_in_rel_v2 = True
1466 repeated_face = False
1467 # If two verts have two related verts in common, they form a quad.
1468 if len(related_verts_in_common) == 2:
1469 # Check if the face is already saved.
1470 for f_verts in faces_verts_idx:
1471 repeated_verts = 0
1473 if len(f_verts) == 4:
1474 if v1 in f_verts: repeated_verts += 1
1475 if v2 in f_verts: repeated_verts += 1
1476 if related_verts_in_common[0] in f_verts: repeated_verts += 1
1477 if related_verts_in_common[1] in f_verts: repeated_verts += 1
1479 if repeated_verts == len(f_verts):
1480 repeated_face = True
1481 break
1483 if not repeated_face:
1484 faces_verts_idx.append([v1, related_verts_in_common[0], v2, related_verts_in_common[1]])
1486 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1: # If Two verts have one related vert in common and they are related to each other, they form a triangle.
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) == 3:
1492 if v1 in f_verts: repeated_verts += 1
1493 if v2 in f_verts: repeated_verts += 1
1494 if related_verts_in_common[0] in f_verts: repeated_verts += 1
1496 if repeated_verts == len(f_verts):
1497 repeated_face = True
1498 break
1500 if not repeated_face:
1501 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1504 #### Keep only the faces that don't overlap by ignoring quads that overlap with two adjacent triangles.
1505 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate.
1506 for i in range(len(faces_verts_idx)):
1507 for t in range(len(faces_verts_idx)):
1508 if i != t:
1509 verts_in_common = 0
1511 if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
1512 for v_idx in faces_verts_idx[t]:
1513 if v_idx in faces_verts_idx[i]:
1514 verts_in_common += 1
1516 if verts_in_common == 3: # If it doesn't have all it's vertices repeated in the other face.
1517 if not i in faces_to_not_include_idx:
1518 faces_to_not_include_idx.append(i)
1521 #### Build surface.
1522 all_surface_verts_co = []
1523 verts_idx_translation = {}
1524 for i in range(len(final_points_ob.data.vertices)):
1525 coords = final_points_ob.data.vertices[i].co
1526 all_surface_verts_co.append([coords[0], coords[1], coords[2]])
1528 # Verts of each face.
1529 all_surface_faces = []
1530 for i in range(len(faces_verts_idx)):
1531 if not i in faces_to_not_include_idx:
1532 face = []
1533 for v_idx in faces_verts_idx[i]:
1534 face.append(v_idx)
1536 all_surface_faces.append(face)
1538 # Build the mesh.
1539 surf_me_name = "SURFSKIO_surface"
1540 me_surf = bpy.data.meshes.new(surf_me_name)
1542 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
1544 me_surf.update()
1546 ob_surface = bpy.data.objects.new(surf_me_name, me_surf)
1547 bpy.context.scene.objects.link(ob_surface)
1549 # Delete final points temporal object
1550 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1551 bpy.data.objects[final_points_ob.name].select = True
1552 bpy.context.scene.objects.active = bpy.data.objects[final_points_ob.name]
1554 bpy.ops.object.delete()
1557 # Delete isolated verts if there are any.
1558 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1559 bpy.data.objects[ob_surface.name].select = True
1560 bpy.context.scene.objects.active = bpy.data.objects[ob_surface.name]
1562 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1563 bpy.ops.mesh.select_all(action='DESELECT')
1564 bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
1565 bpy.ops.mesh.delete()
1566 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1570 #### Join crosshatch results with original mesh.
1572 # Calculate a distance to merge the verts of the crosshatch surface to the main object.
1573 edges_length_sum = 0
1574 for ed in ob_surface.data.edges:
1575 edges_length_sum += (ob_surface.data.vertices[ed.vertices[0]].co - ob_surface.data.vertices[ed.vertices[1]].co).length
1577 if len(ob_surface.data.edges) > 0:
1578 average_surface_edges_length = edges_length_sum / len(ob_surface.data.edges)
1579 else:
1580 average_surface_edges_length = 0.0001
1582 # Make dictionary with all the verts connected to each vert, on the new surface object.
1583 surface_connected_verts = {}
1584 for ed in ob_surface.data.edges:
1585 if not ed.vertices[0] in surface_connected_verts:
1586 surface_connected_verts[ed.vertices[0]] = []
1588 surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
1591 if not ed.vertices[1] in surface_connected_verts:
1592 surface_connected_verts[ed.vertices[1]] = []
1594 surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
1598 # Duplicate the new surface object, and use shrinkwrap to calculate later the nearest verts to the main object.
1599 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1600 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1601 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1603 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1605 final_ob_duplicate = bpy.context.scene.objects.active
1607 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1608 final_ob_duplicate.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX"
1609 final_ob_duplicate.modifiers["Shrinkwrap"].target = self.main_object
1611 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier='Shrinkwrap')
1614 # Make list with verts of original mesh as index and coords as value.
1615 main_object_verts_coords = []
1616 for v in self.main_object.data.vertices:
1617 coords = self.main_object.matrix_world * v.co
1619 for c in range(len(coords)): # To avoid problems when taking "-0.00" as a different value as "0.00".
1620 if "%.3f" % coords[c] == "-0.00":
1621 coords[c] = 0
1623 main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
1625 tuple(main_object_verts_coords)
1628 # Determine which verts will be merged, snap them to the nearest verts on the original verts, and get them selected.
1629 crosshatch_verts_to_merge = []
1630 if self.automatic_join:
1631 for i in range(len(ob_surface.data.vertices)):
1632 # Calculate the distance from each of the connected verts to the actual vert, and compare it with the distance they would have if joined. If they don't change much, that vert can be joined.
1633 merge_actual_vert = True
1634 if len(surface_connected_verts[i]) < 4:
1635 for c_v_idx in surface_connected_verts[i]:
1636 points_original = []
1637 points_original.append(ob_surface.data.vertices[c_v_idx].co)
1638 points_original.append(ob_surface.data.vertices[i].co)
1640 points_target = []
1641 points_target.append(ob_surface.data.vertices[c_v_idx].co)
1642 points_target.append(final_ob_duplicate.data.vertices[i].co)
1644 vec_A = points_original[0] - points_original[1]
1645 vec_B = points_target[0] - points_target[1]
1647 dist_A = (points_original[0] - points_original[1]).length
1648 dist_B = (points_target[0] - points_target[1]).length
1651 if not (points_original[0] == points_original[1] or points_target[0] == points_target[1]): # If any vector's length is zero.
1652 angle = vec_A.angle(vec_B) / math.pi
1653 else:
1654 angle= 0
1657 if dist_B > dist_A * 1.7 * self.join_stretch_factor or dist_B < dist_A / 2 / self.join_stretch_factor or angle >= 0.15 * self.join_stretch_factor: # Set a range of acceptable variation in the connected edges.
1658 merge_actual_vert = False
1659 break
1660 else:
1661 merge_actual_vert = False
1664 if merge_actual_vert:
1665 coords = final_ob_duplicate.data.vertices[i].co
1667 for c in range(len(coords)): # To avoid problems when taking "-0.000" as a different value as "0.00".
1668 if "%.3f" % coords[c] == "-0.00":
1669 coords[c] = 0
1671 comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
1674 if comparison_coords in main_object_verts_coords:
1675 main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords) # Get the index of the vert with those coords in the main object.
1677 if self.main_object.data.vertices[main_object_related_vert_idx].select == True or self.main_object_selected_verts_count == 0:
1678 ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
1679 ob_surface.data.vertices[i].select = True
1680 crosshatch_verts_to_merge.append(i)
1682 # Make sure the vert in the main object is selected, in case it wasn't selected and the "join crosshatch" option is active.
1683 self.main_object.data.vertices[main_object_related_vert_idx].select = True
1688 # Delete duplicated object.
1689 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1690 bpy.data.objects[final_ob_duplicate.name].select = True
1691 bpy.context.scene.objects.active = bpy.data.objects[final_ob_duplicate.name]
1692 bpy.ops.object.delete()
1695 # Join crosshatched surface and main object.
1696 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1697 bpy.data.objects[ob_surface.name].select = True
1698 bpy.data.objects[self.main_object.name].select = True
1699 bpy.context.scene.objects.active = bpy.data.objects[self.main_object.name]
1701 bpy.ops.object.join('INVOKE_REGION_WIN')
1703 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1704 # Perform Remove doubles to merge verts.
1705 if not (self.automatic_join == False and self.main_object_selected_verts_count == 0):
1706 bpy.ops.mesh.remove_doubles(threshold=0.0001)
1708 bpy.ops.mesh.select_all(action='DESELECT')
1711 #### If the main object has modifiers, turn their "viewport view status" to what it was before the forced deactivation above.
1712 if len(self.main_object.modifiers) > 0:
1713 for m_idx in range(len(self.main_object.modifiers)):
1714 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1718 return{'FINISHED'}
1722 def rectangular_surface(self):
1723 #### Selected edges.
1724 all_selected_edges_idx = []
1725 all_selected_verts = []
1726 all_verts_idx = []
1727 for ed in self.main_object.data.edges:
1728 if ed.select:
1729 all_selected_edges_idx.append(ed.index)
1731 # Selected vertices.
1732 if not ed.vertices[0] in all_selected_verts:
1733 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
1734 if not ed.vertices[1] in all_selected_verts:
1735 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
1737 # All verts (both from each edge) to determine later which are at the tips (those not repeated twice).
1738 all_verts_idx.append(ed.vertices[0])
1739 all_verts_idx.append(ed.vertices[1])
1743 #### Identify the tips and "middle-vertex" that separates U from V, if there is one.
1744 all_chains_tips_idx = []
1745 for v_idx in all_verts_idx:
1746 if all_verts_idx.count(v_idx) < 2:
1747 all_chains_tips_idx.append(v_idx)
1751 edges_connected_to_tips = []
1752 for ed in self.main_object.data.edges:
1753 if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
1754 edges_connected_to_tips.append(ed)
1757 #### Check closed selections.
1758 single_unselected_verts_and_neighbors = [] # List with groups of three verts, where the first element of the pair is the unselected vert of a closed selection and the other two elements are the selected neighbor verts (it will be useful to determine which selection chain the unselected vert belongs to, and determine the "middle-vertex")
1760 # To identify a "closed" selection (a selection that is a closed chain except for one vertex) find the vertex in common that have the edges connected to tips. If there is a vertex in common, that one is the unselected vert that closes the selection or is a "middle-vertex".
1761 single_unselected_verts = []
1762 for ed in edges_connected_to_tips:
1763 for ed_b in edges_connected_to_tips:
1764 if ed != ed_b:
1765 if ed.vertices[0] == ed_b.vertices[0] and not self.main_object.data.vertices[ed.vertices[0]].select and ed.vertices[0] not in single_unselected_verts:
1766 single_unselected_verts_and_neighbors.append([ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]) # The second element is one of the tips of the selected vertices of the closed selection.
1767 single_unselected_verts.append(ed.vertices[0])
1768 break
1769 elif ed.vertices[0] == ed_b.vertices[1] and not self.main_object.data.vertices[ed.vertices[0]].select and ed.vertices[0] not in single_unselected_verts:
1770 single_unselected_verts_and_neighbors.append([ed.vertices[0], ed.vertices[1], ed_b.vertices[0]])
1771 single_unselected_verts.append(ed.vertices[0])
1772 break
1773 elif ed.vertices[1] == ed_b.vertices[0] and not self.main_object.data.vertices[ed.vertices[1]].select and ed.vertices[1] not in single_unselected_verts:
1774 single_unselected_verts_and_neighbors.append([ed.vertices[1], ed.vertices[0], ed_b.vertices[1]])
1775 single_unselected_verts.append(ed.vertices[1])
1776 break
1777 elif ed.vertices[1] == ed_b.vertices[1] and not self.main_object.data.vertices[ed.vertices[1]].select and ed.vertices[1] not in single_unselected_verts:
1778 single_unselected_verts_and_neighbors.append([ed.vertices[1], ed.vertices[0], ed_b.vertices[0]])
1779 single_unselected_verts.append(ed.vertices[1])
1780 break
1783 middle_vertex_idx = None
1784 tips_to_discard_idx = []
1785 # Check if there is a "middle-vertex", and get its index.
1786 for i in range(0, len(single_unselected_verts_and_neighbors)):
1787 actual_chain_verts = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, single_unselected_verts_and_neighbors[i][1], None, None)
1789 if single_unselected_verts_and_neighbors[i][2] != actual_chain_verts[len(actual_chain_verts) - 1].index:
1790 middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
1791 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
1792 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
1795 #### List with pairs of verts that belong to the tips of each selection chain (row).
1796 verts_tips_same_chain_idx = []
1797 if len(all_chains_tips_idx) >= 2:
1798 checked_v = []
1799 for i in range(0, len(all_chains_tips_idx)):
1800 if all_chains_tips_idx[i] not in checked_v:
1801 v_chain = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, all_chains_tips_idx[i], middle_vertex_idx, None)
1803 verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
1805 checked_v.append(v_chain[0].index)
1806 checked_v.append(v_chain[len(v_chain) - 1].index)
1809 #### Selection tips (vertices).
1810 verts_tips_parsed_idx = []
1811 if len(all_chains_tips_idx) >= 2:
1812 for spec_v_idx in all_chains_tips_idx:
1813 if (spec_v_idx not in tips_to_discard_idx):
1814 verts_tips_parsed_idx.append(spec_v_idx)
1817 #### Identify the type of selection made by the user.
1818 if middle_vertex_idx != None:
1819 if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1: # If there are 4 tips (two selection chains), and there is only one single unselected vert (the middle vert).
1820 selection_type = "TWO_CONNECTED"
1821 else:
1822 # The type of the selection was not identified, the script stops.
1823 self.report({'WARNING'}, "The selection isn't valid.")
1824 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1825 self.cleanup_on_interruption()
1826 self.stopping_errors = True
1828 return{'CANCELLED'}
1829 else:
1830 if len(all_chains_tips_idx) == 2: # If there are 2 tips
1831 selection_type = "SINGLE"
1832 elif len(all_chains_tips_idx) == 4: # If there are 4 tips
1833 selection_type = "TWO_NOT_CONNECTED"
1834 elif len(all_chains_tips_idx) == 0:
1835 if len(self.main_splines.data.splines) > 1:
1836 selection_type = "NO_SELECTION"
1837 else:
1838 # If the selection was not identified and there is only one stroke, there's no possibility to build a surface, so the script is interrupted.
1839 self.report({'WARNING'}, "The selection isn't valid.")
1840 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1841 self.cleanup_on_interruption()
1842 self.stopping_errors = True
1844 return{'CANCELLED'}
1845 else:
1846 # The type of the selection was not identified, the script stops.
1847 self.report({'WARNING'}, "The selection isn't valid.")
1849 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1850 self.cleanup_on_interruption()
1852 self.stopping_errors = True
1854 return{'CANCELLED'}
1858 #### If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script.
1859 if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
1860 self.report({'WARNING'}, "At least two strokes are needed when there are two not connected selections.")
1861 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1862 self.cleanup_on_interruption()
1863 self.stopping_errors = True
1865 return{'CANCELLED'}
1869 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1871 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1872 bpy.data.objects[self.main_splines.name].select = True
1873 bpy.context.scene.objects.active = bpy.context.scene.objects[self.main_splines.name]
1876 #### Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out.
1877 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1878 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1879 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1880 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1881 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1882 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1883 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1884 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1887 self.selection_U_exists = False
1888 self.selection_U2_exists = False
1889 self.selection_V_exists = False
1890 self.selection_V2_exists = False
1892 self.selection_U_is_closed = False
1893 self.selection_U2_is_closed = False
1894 self.selection_V_is_closed = False
1895 self.selection_V2_is_closed = False
1897 #### Define what vertices are at the tips of each selection and are not the middle-vertex.
1898 if selection_type == "TWO_CONNECTED":
1899 self.selection_U_exists = True
1900 self.selection_V_exists = True
1902 closing_vert_U_idx = None
1903 closing_vert_V_idx = None
1904 closing_vert_U2_idx = None
1905 closing_vert_V2_idx = None
1907 # Determine which selection is Selection-U and which is Selection-V.
1908 points_A = []
1909 points_B = []
1910 points_first_stroke_tips = []
1912 points_A.append(self.main_object.matrix_world * self.main_object.data.vertices[verts_tips_parsed_idx[0]].co)
1913 points_A.append(self.main_object.matrix_world * self.main_object.data.vertices[middle_vertex_idx].co)
1915 points_B.append(self.main_object.matrix_world * self.main_object.data.vertices[verts_tips_parsed_idx[1]].co)
1916 points_B.append(self.main_object.matrix_world * self.main_object.data.vertices[middle_vertex_idx].co)
1918 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
1919 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[len(self.main_splines.data.splines[0].bezier_points) - 1].co)
1921 angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
1922 angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
1924 if angle_A < angle_B:
1925 first_vert_U_idx = verts_tips_parsed_idx[0]
1926 first_vert_V_idx = verts_tips_parsed_idx[1]
1927 else:
1928 first_vert_U_idx = verts_tips_parsed_idx[1]
1929 first_vert_V_idx = verts_tips_parsed_idx[0]
1931 elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
1932 first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
1933 last_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[len(self.main_splines.data.splines[0].bezier_points) - 1].co
1934 first_sketched_point_last_stroke_co = self.main_splines.data.splines[len(self.main_splines.data.splines) - 1].bezier_points[0].co
1935 if len(self.main_splines.data.splines) > 1:
1936 first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
1937 last_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[len(self.main_splines.data.splines[1].bezier_points) - 1].co
1940 single_unselected_neighbors = [] # Only the neighbors of the single unselected verts.
1941 for verts_neig_idx in single_unselected_verts_and_neighbors:
1942 single_unselected_neighbors.append(verts_neig_idx[1])
1943 single_unselected_neighbors.append(verts_neig_idx[2])
1946 all_chains_tips_and_middle_vert = []
1947 for v_idx in all_chains_tips_idx:
1948 if v_idx not in single_unselected_neighbors:
1949 all_chains_tips_and_middle_vert.append(v_idx)
1952 all_chains_tips_and_middle_vert += single_unselected_verts
1954 all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
1956 # The tip of the selected vertices nearest to the first point of the first sketched stroke.
1957 nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = self.shortest_distance(self.main_object, first_sketched_point_first_stroke_co, all_chains_tips_and_middle_vert)
1958 # If the nearest tip is not from a closed selection, get the opposite tip vertex index.
1959 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
1960 nearest_tip_to_first_st_first_pt_opposite_idx = self.opposite_tip(nearest_tip_to_first_st_first_pt_idx, verts_tips_same_chain_idx)
1962 # The tip of the selected vertices nearest to the last point of the first sketched stroke.
1963 nearest_tip_to_first_st_last_pt_idx, temp_dist = self.shortest_distance(self.main_object, last_sketched_point_first_stroke_co, all_chains_tips_and_middle_vert)
1965 # The tip of the selected vertices nearest to the first point of the last sketched stroke.
1966 nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = self.shortest_distance(self.main_object, first_sketched_point_last_stroke_co, all_chains_tips_and_middle_vert)
1968 if len(self.main_splines.data.splines) > 1:
1969 # The selected vertex nearest to the first point of the second sketched stroke. (This will be useful to determine the direction of the closed selection V when extruding along strokes)
1970 nearest_vert_to_second_st_first_pt_idx, temp_dist = self.shortest_distance(self.main_object, first_sketched_point_second_stroke_co, all_verts_idx)
1972 # The selected vertex nearest to the first point of the second sketched stroke. (This will be useful to determine the direction of the closed selection V2 when extruding along strokes)
1973 nearest_vert_to_second_st_last_pt_idx, temp_dist = self.shortest_distance(self.main_object, last_sketched_point_second_stroke_co, all_verts_idx)
1977 # Determine if the single selection will be treated as U or as V.
1978 edges_sum = 0
1979 for i in all_selected_edges_idx:
1980 edges_sum += ((self.main_object.matrix_world * self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) - (self.main_object.matrix_world * self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)).length
1982 average_edge_length = edges_sum / len(all_selected_edges_idx)
1985 # Get shortest distance from the first point of the last stroke to any participating vertex.
1986 temp_idx, shortest_distance_to_last_stroke = self.shortest_distance(self.main_object, first_sketched_point_last_stroke_co, all_participating_verts)
1989 if shortest_distance_to_first_stroke < average_edge_length / 4 and shortest_distance_to_last_stroke < average_edge_length and len(self.main_splines.data.splines) > 1: # If the beginning of the first stroke is near enough, and its orientation difference with the first edge of the nearest selection chain is not too high, interpret things as an "extrude along strokes" instead of "extrude through strokes"
1990 self.selection_U_exists = False
1991 self.selection_V_exists = True
1992 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx: # If the first selection is not closed.
1993 self.selection_V_is_closed = False
1994 first_neighbor_V_idx = None
1995 closing_vert_U_idx = None
1996 closing_vert_U2_idx = None
1997 closing_vert_V_idx = None
1998 closing_vert_V2_idx = None
2000 first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2002 if selection_type == "TWO_NOT_CONNECTED":
2003 self.selection_V2_exists = True
2005 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2006 else:
2007 self.selection_V_is_closed = True
2008 closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2010 # Get the neighbors of the first (unselected) vert of the closed selection U.
2011 vert_neighbors = []
2012 for verts in single_unselected_verts_and_neighbors:
2013 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2014 vert_neighbors.append(verts[1])
2015 vert_neighbors.append(verts[2])
2016 break
2018 verts_V = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, vert_neighbors[0], middle_vertex_idx, None)
2020 for i in range(0, len(verts_V)):
2021 if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
2022 if i >= len(verts_V) / 2: # If the vertex nearest to the first point of the second stroke is in the first half of the selected verts.
2023 first_vert_V_idx = vert_neighbors[1]
2024 break
2025 else:
2026 first_vert_V_idx = vert_neighbors[0]
2027 break
2031 if selection_type == "TWO_NOT_CONNECTED":
2032 self.selection_V2_exists = True
2034 if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx: # If the second selection is not closed.
2035 self.selection_V2_is_closed = False
2036 first_neighbor_V2_idx = None
2037 closing_vert_V2_idx = None
2039 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2041 else:
2042 self.selection_V2_is_closed = True
2043 closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2045 # Get the neighbors of the first (unselected) vert of the closed selection U.
2046 vert_neighbors = []
2047 for verts in single_unselected_verts_and_neighbors:
2048 if verts[0] == nearest_tip_to_first_st_last_pt_idx:
2049 vert_neighbors.append(verts[1])
2050 vert_neighbors.append(verts[2])
2051 break
2054 verts_V2 = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, vert_neighbors[0], middle_vertex_idx, None)
2056 for i in range(0, len(verts_V2)):
2057 if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
2058 if i >= len(verts_V2) / 2: # If the vertex nearest to the first point of the second stroke is in the first half of the selected verts.
2059 first_vert_V2_idx = vert_neighbors[1]
2060 break
2061 else:
2062 first_vert_V2_idx = vert_neighbors[0]
2063 break
2065 else:
2066 self.selection_V2_exists = False
2068 else:
2069 self.selection_U_exists = True
2070 self.selection_V_exists = False
2071 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx: # If the first selection is not closed.
2072 self.selection_U_is_closed = False
2073 first_neighbor_U_idx = None
2074 closing_vert_U_idx = None
2076 points_tips = []
2077 points_tips.append(self.main_object.matrix_world * self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co)
2078 points_tips.append(self.main_object.matrix_world * self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co)
2080 points_first_stroke_tips = []
2081 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2082 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[len(self.main_splines.data.splines[0].bezier_points) - 1].co)
2084 vec_A = points_tips[0] - points_tips[1]
2085 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2087 # Compare the direction of the selection and the first grease pencil stroke to determine which is the "first" vertex of the selection.
2088 if vec_A.dot(vec_B) < 0:
2089 first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
2090 else:
2091 first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2093 else:
2094 self.selection_U_is_closed = True
2095 closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2097 # Get the neighbors of the first (unselected) vert of the closed selection U.
2098 vert_neighbors = []
2099 for verts in single_unselected_verts_and_neighbors:
2100 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2101 vert_neighbors.append(verts[1])
2102 vert_neighbors.append(verts[2])
2103 break
2105 points_first_and_neighbor = []
2106 points_first_and_neighbor.append(self.main_object.matrix_world * self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co)
2107 points_first_and_neighbor.append(self.main_object.matrix_world * self.main_object.data.vertices[vert_neighbors[0]].co)
2109 points_first_stroke_tips = []
2110 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2111 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
2113 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2114 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2116 # Compare the direction of the selection and the first grease pencil stroke to determine which is the vertex neighbor to the first vertex (unselected) of the closed selection. This will determine the direction of the closed selection.
2117 if vec_A.dot(vec_B) < 0:
2118 first_vert_U_idx = vert_neighbors[1]
2119 else:
2120 first_vert_U_idx = vert_neighbors[0]
2124 if selection_type == "TWO_NOT_CONNECTED":
2125 self.selection_U2_exists = True
2127 if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx: # If the second selection is not closed.
2128 self.selection_U2_is_closed = False
2129 first_neighbor_U2_idx = None
2130 closing_vert_U2_idx = None
2132 first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2134 else:
2135 self.selection_U2_is_closed = True
2136 closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2138 # Get the neighbors of the first (unselected) vert of the closed selection U.
2139 vert_neighbors = []
2140 for verts in single_unselected_verts_and_neighbors:
2141 if verts[0] == nearest_tip_to_last_st_first_pt_idx:
2142 vert_neighbors.append(verts[1])
2143 vert_neighbors.append(verts[2])
2144 break
2146 points_first_and_neighbor = []
2147 points_first_and_neighbor.append(self.main_object.matrix_world * self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co)
2148 points_first_and_neighbor.append(self.main_object.matrix_world * self.main_object.data.vertices[vert_neighbors[0]].co)
2150 points_last_stroke_tips = []
2151 points_last_stroke_tips.append(self.main_splines.data.splines[len(self.main_splines.data.splines) - 1].bezier_points[0].co)
2152 points_last_stroke_tips.append(self.main_splines.data.splines[len(self.main_splines.data.splines) - 1].bezier_points[1].co)
2154 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2155 vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
2157 # Compare the direction of the selection and the last grease pencil stroke to determine which is the vertex neighbor to the first vertex (unselected) of the closed selection. This will determine the direction of the closed selection.
2158 if vec_A.dot(vec_B) < 0:
2159 first_vert_U2_idx = vert_neighbors[1]
2160 else:
2161 first_vert_U2_idx = vert_neighbors[0]
2163 else:
2164 self.selection_U2_exists = False
2166 elif selection_type == "NO_SELECTION":
2167 self.selection_U_exists = False
2168 self.selection_V_exists = False
2172 #### Get an ordered list of the vertices of Selection-U.
2173 verts_ordered_U = []
2174 if self.selection_U_exists:
2175 verts_ordered_U = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, first_vert_U_idx, middle_vertex_idx, closing_vert_U_idx)
2176 verts_ordered_U_indices = [x.index for x in verts_ordered_U]
2178 #### Get an ordered list of the vertices of Selection-U2.
2179 verts_ordered_U2 = []
2180 if self.selection_U2_exists:
2181 verts_ordered_U2 = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, first_vert_U2_idx, middle_vertex_idx, closing_vert_U2_idx)
2182 verts_ordered_U2_indices = [x.index for x in verts_ordered_U2]
2184 #### Get an ordered list of the vertices of Selection-V.
2185 verts_ordered_V = []
2186 if self.selection_V_exists:
2187 verts_ordered_V = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, first_vert_V_idx, middle_vertex_idx, closing_vert_V_idx)
2188 verts_ordered_V_indices = [x.index for x in verts_ordered_V]
2190 #### Get an ordered list of the vertices of Selection-V2.
2191 verts_ordered_V2 = []
2192 if self.selection_V2_exists:
2193 verts_ordered_V2 = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, first_vert_V2_idx, middle_vertex_idx, closing_vert_V2_idx)
2194 verts_ordered_V2_indices = [x.index for x in verts_ordered_V2]
2198 #### Check if when there are two-not-connected selections both have the same number of verts. If not terminate the script.
2199 if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or (self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
2200 # Display a warning.
2201 self.report({'WARNING'}, "Both selections must have the same number of edges")
2203 self.cleanup_on_interruption()
2205 self.stopping_errors = True
2207 return{'CANCELLED'}
2211 #### Calculate edges U proportions.
2213 # Sum selected edges U lengths.
2214 edges_lengths_U = []
2215 edges_lengths_sum_U = 0
2217 if self.selection_U_exists:
2218 edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(self.main_object, verts_ordered_U)
2220 if self.selection_U2_exists:
2221 edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(self.main_object, verts_ordered_U2)
2223 # Sum selected edges V lengths.
2224 edges_lengths_V = []
2225 edges_lengths_sum_V = 0
2227 if self.selection_V_exists:
2228 edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(self.main_object, verts_ordered_V)
2230 if self.selection_V2_exists:
2231 edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(self.main_object, verts_ordered_V2)
2234 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2235 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts = bpy.context.scene.SURFSK_precision)
2236 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2239 # Proportions U.
2240 edges_proportions_U = []
2241 edges_proportions_U = self.get_edges_proportions(edges_lengths_U, edges_lengths_sum_U, self.selection_U_exists, self.edges_U)
2242 verts_count_U = len(edges_proportions_U) + 1
2244 if self.selection_U2_exists:
2245 edges_proportions_U2 = []
2246 edges_proportions_U2 = self.get_edges_proportions(edges_lengths_U2, edges_lengths_sum_U2, self.selection_U2_exists, self.edges_V)
2247 verts_count_U2 = len(edges_proportions_U2) + 1
2249 # Proportions V.
2250 edges_proportions_V = []
2251 edges_proportions_V = self.get_edges_proportions(edges_lengths_V, edges_lengths_sum_V, self.selection_V_exists, self.edges_V)
2252 verts_count_V = len(edges_proportions_V) + 1
2254 if self.selection_V2_exists:
2255 edges_proportions_V2 = []
2256 edges_proportions_V2 = self.get_edges_proportions(edges_lengths_V2, edges_lengths_sum_V2, self.selection_V2_exists, self.edges_V)
2257 verts_count_V2 = len(edges_proportions_V2) + 1
2266 #### Cyclic Follow: simplify sketched curves, make them Cyclic, and complete the actual sketched curves with a "closing segment".
2267 if self.cyclic_follow and not self.selection_V_exists and not ((self.selection_U_exists and not self.selection_U_is_closed) or (self.selection_U2_exists and not self.selection_U2_is_closed)):
2268 simplified_spline_coords = []
2269 simplified_curve = []
2270 ob_simplified_curve = []
2271 splines_first_v_co = []
2272 for i in range(len(self.main_splines.data.splines)):
2273 # Create a curve object for the actual spline "cyclic extension".
2274 simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
2275 ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
2276 bpy.context.scene.objects.link(ob_simplified_curve[i])
2278 simplified_curve[i].dimensions = "3D"
2280 spline_coords = []
2281 for bp in self.main_splines.data.splines[i].bezier_points:
2282 spline_coords.append(bp.co)
2284 # Simplification.
2285 simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
2287 # Get the coordinates of the first vert of the actual spline.
2288 splines_first_v_co.append(simplified_spline_coords[i][0])
2291 # Generate the spline.
2292 spline = simplified_curve[i].splines.new('BEZIER')
2293 spline.bezier_points.add(len(simplified_spline_coords[i]) - 1) # less one because one point is added when the spline is created.
2294 for p in range(0, len(simplified_spline_coords[i])):
2295 spline.bezier_points[p].co = simplified_spline_coords[i][p]
2298 spline.use_cyclic_u = True
2300 spline_bp_count = len(spline.bezier_points)
2302 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2303 bpy.data.objects[ob_simplified_curve[i].name].select = True
2304 bpy.context.scene.objects.active = bpy.context.scene.objects[ob_simplified_curve[i].name]
2306 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2307 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
2308 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2309 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
2310 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2313 # Select the "closing segment", and subdivide it.
2314 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
2315 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
2316 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
2318 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
2319 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
2320 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
2322 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2323 segments = sqrt((ob_simplified_curve[i].data.splines[0].bezier_points[0].co - ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length / self.average_gp_segment_length)
2324 for t in range(2):
2325 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts = segments)
2328 # Delete the other vertices and make it non-cyclic to keep only the needed verts of the "closing segment".
2329 bpy.ops.curve.select_all(action = 'INVERT')
2330 bpy.ops.curve.delete(type='VERT')
2331 ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
2332 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2335 # Add the points of the "closing segment" to the original curve from grease pencil stroke.
2336 first_new_index = len(self.main_splines.data.splines[i].bezier_points)
2337 self.main_splines.data.splines[i].bezier_points.add(len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1)
2338 for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
2339 self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = ob_simplified_curve[i].data.splines[0].bezier_points[t].co
2342 # Delete the temporal curve.
2343 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2344 bpy.data.objects[ob_simplified_curve[i].name].select = True
2345 bpy.context.scene.objects.active = bpy.context.scene.objects[ob_simplified_curve[i].name]
2347 bpy.ops.object.delete()
2351 #### Get the coords of the points distributed along the sketched strokes, with proportions-U of the first selection.
2352 pts_on_strokes_with_proportions_U = self.distribute_pts(self.main_splines.data.splines, edges_proportions_U)
2354 sketched_splines_parsed = []
2356 if self.selection_U2_exists:
2357 # Initialize the multidimensional list with the proportions of all the segments.
2358 proportions_loops_crossing_strokes = []
2359 for i in range(len(pts_on_strokes_with_proportions_U)):
2360 proportions_loops_crossing_strokes.append([])
2362 for t in range(len(pts_on_strokes_with_proportions_U[0])):
2363 proportions_loops_crossing_strokes[i].append(None)
2366 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U.
2367 for lp in range(len(pts_on_strokes_with_proportions_U[0])):
2368 loop_segments_lengths = []
2370 for st in range(len(pts_on_strokes_with_proportions_U)):
2371 if st == 0: # When on the first stroke, add the segment from the selection to the dirst stroke.
2372 loop_segments_lengths.append(((self.main_object.matrix_world * verts_ordered_U[lp].co) - pts_on_strokes_with_proportions_U[0][lp]).length)
2374 if st != len(pts_on_strokes_with_proportions_U) - 1: # For all strokes except for the last, calculate the distance from the actual stroke to the next.
2375 loop_segments_lengths.append((pts_on_strokes_with_proportions_U[st][lp] - pts_on_strokes_with_proportions_U[st + 1][lp]).length)
2377 if st == len(pts_on_strokes_with_proportions_U) - 1: # When on the last stroke, add the segments from the last stroke to the second selection.
2378 loop_segments_lengths.append((pts_on_strokes_with_proportions_U[st][lp] - (self.main_object.matrix_world * verts_ordered_U2[lp].co)).length)
2380 # Calculate full loop length.
2381 loop_seg_lengths_sum = 0
2382 for i in range(len(loop_segments_lengths)):
2383 loop_seg_lengths_sum += loop_segments_lengths[i]
2385 # Fill the multidimensional list with the proportions of all the segments.
2386 for st in range(len(pts_on_strokes_with_proportions_U)):
2387 proportions_loops_crossing_strokes[st][lp] = loop_segments_lengths[st] / loop_seg_lengths_sum
2390 # Calculate proportions for each stroke.
2391 for st in range(len(pts_on_strokes_with_proportions_U)):
2392 actual_stroke_spline = []
2393 actual_stroke_spline.append(self.main_splines.data.splines[st]) # Needs to be a list for the "distribute_pts" method.
2395 # Calculate the proportions for the actual stroke.
2396 actual_edges_proportions_U = []
2397 for i in range(len(edges_proportions_U)):
2398 proportions_sum = 0
2400 # Sum the proportions of this loop up to the actual.
2401 for t in range(0, st + 1):
2402 proportions_sum += proportions_loops_crossing_strokes[t][i]
2404 actual_edges_proportions_U.append(edges_proportions_U[i] - ((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)) # i + 1, because proportions_loops_crossing_strokes refers to loops, and the proportions refer to edges, so we start at the element 1 of proportions_loops_crossing_strokes instead of element 0.
2407 points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
2408 sketched_splines_parsed.append(points_actual_spline[0])
2410 else:
2411 sketched_splines_parsed = pts_on_strokes_with_proportions_U
2415 #### If the selection type is "TWO_NOT_CONNECTED" replace the points of the last spline with the points in the "target" selection.
2416 if selection_type == "TWO_NOT_CONNECTED":
2417 if self.selection_U2_exists:
2418 for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
2419 sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = self.main_object.matrix_world * verts_ordered_U2[i].co
2422 #### Create temporary curves along the "control-points" found on the sketched curves and the mesh selection.
2423 mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
2424 me = bpy.data.meshes.new(mesh_ctrl_pts_name)
2425 ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
2426 ob_ctrl_pts.data = me
2427 bpy.context.scene.objects.link(ob_ctrl_pts)
2430 cyclic_loops_U = []
2431 first_verts = []
2432 second_verts = []
2433 last_verts = []
2434 for i in range(0, verts_count_U):
2435 vert_num_in_spline = 1
2437 if self.selection_U_exists:
2438 ob_ctrl_pts.data.vertices.add(1)
2439 last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2440 last_v.co = self.main_object.matrix_world * verts_ordered_U[i].co
2442 vert_num_in_spline += 1
2445 for t in range(0, len(sketched_splines_parsed)):
2446 ob_ctrl_pts.data.vertices.add(1)
2447 v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2448 v.co = sketched_splines_parsed[t][i]
2451 if vert_num_in_spline > 1:
2452 ob_ctrl_pts.data.edges.add(1)
2453 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = len(ob_ctrl_pts.data.vertices) - 2
2454 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = len(ob_ctrl_pts.data.vertices) - 1
2456 if t == 0:
2457 first_verts.append(v.index)
2459 if t == 1:
2460 second_verts.append(v.index)
2462 if t == len(sketched_splines_parsed) - 1:
2463 last_verts.append(v.index)
2466 last_v = v
2468 vert_num_in_spline += 1
2471 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2472 bpy.data.objects[ob_ctrl_pts.name].select = True
2473 bpy.context.scene.objects.active = bpy.data.objects[ob_ctrl_pts.name]
2475 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2476 bpy.ops.mesh.select_all(action='DESELECT')
2477 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2480 #### Determine which loops-U will be "Cyclic".
2481 for i in range(0, len(first_verts)):
2482 if self.automatic_join and not self.cyclic_cross and selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3: # When there is Cyclic Cross there is no need of Automatic Join, (and there are at least three strokes).
2483 v = ob_ctrl_pts.data.vertices
2485 first_point_co = v[first_verts[i]].co
2486 second_point_co = v[second_verts[i]].co
2487 last_point_co = v[last_verts[i]].co
2489 # Coordinates of the point in the center of both the first and last verts.
2490 verts_center_co = [(first_point_co[0] + last_point_co[0]) / 2, (first_point_co[1] + last_point_co[1]) / 2, (first_point_co[2] + last_point_co[2]) / 2]
2492 vec_A = second_point_co - first_point_co
2493 vec_B = second_point_co - mathutils.Vector(verts_center_co)
2496 # Calculate the length of the first segment of the loop, and the length it would have after moving the first vert to the middle position between first and last.
2497 length_original = (second_point_co - first_point_co).length
2498 length_target = (second_point_co - mathutils.Vector(verts_center_co)).length
2500 angle = vec_A.angle(vec_B) / math.pi
2503 if length_target <= length_original * 1.03 * self.join_stretch_factor and angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists: # If the target length doesn't stretch too much, and the its angle doesn't change to much either.
2504 cyclic_loops_U.append(True)
2506 # Move the first vert to the center coordinates.
2507 ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
2509 # Select the last verts from Cyclic loops, for later deletion all at once.
2510 v[last_verts[i]].select = True
2512 else:
2513 cyclic_loops_U.append(False)
2515 else:
2516 if self.cyclic_cross and not self.selection_U_exists and not ((self.selection_V_exists and not self.selection_V_is_closed) or (self.selection_V2_exists and not self.selection_V2_is_closed)): # If "Cyclic Cross" is active then "all" crossing curves become cyclic.
2517 cyclic_loops_U.append(True)
2518 else:
2519 cyclic_loops_U.append(False)
2521 # The cyclic_loops_U list needs to be reversed.
2522 cyclic_loops_U.reverse()
2524 # Delete the previously selected (last_)verts.
2525 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2526 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
2527 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2529 # Create curves from control points.
2530 bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
2531 ob_curves_surf = bpy.context.scene.objects.active
2532 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2533 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2534 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2536 # Make Cyclic the splines designated as Cyclic.
2537 for i in range(0, len(cyclic_loops_U)):
2538 ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
2541 #### Get the coords of all points on first loop-U, for later comparison with its subdivided version, to know which points of the loops-U are crossed by the original strokes. The indices wiil be the same for the other loops-U.
2542 if self.loops_on_strokes:
2543 coords_loops_U_control_points = []
2544 for p in ob_ctrl_pts.data.splines[0].bezier_points:
2545 coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
2547 tuple(coords_loops_U_control_points)
2550 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive.
2551 if self.loops_on_strokes and not self.selection_V_exists:
2552 edges_V_count = len(self.main_splines.data.splines) * self.edges_V
2553 else:
2554 edges_V_count = len(edges_proportions_V)
2557 # The Follow precision will vary depending on the number of Follow face-loops.
2558 precision_multiplier = round(2 + (edges_V_count / 15))
2560 curve_cuts = bpy.context.scene.SURFSK_precision * precision_multiplier
2562 # Subdivide the curves.
2563 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts = curve_cuts)
2565 # The verts position shifting that happens with splines subdivision. For later reorder splines points.
2566 verts_position_shift = curve_cuts + 1
2568 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2571 # Reorder coordinates of the points of each spline to put the first point of the spline starting at the position it was the first point before sudividing the curve. And make a new curve object per spline (to handle memory better later).
2572 splines_U_objects = []
2573 for i in range(len(ob_curves_surf.data.splines)):
2574 spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
2575 ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
2576 bpy.context.scene.objects.link(ob_spline_U)
2578 spline_U_curve.dimensions = "3D"
2581 # Add points to the spline in the new curve object.
2582 ob_spline_U.data.splines.new('BEZIER')
2583 for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
2584 if cyclic_loops_U[i] == True and not self.selection_U_exists: # If the loop is cyclic.
2585 if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
2586 point_index = t + verts_position_shift
2587 else:
2588 point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
2589 else:
2590 point_index = t
2592 if t > 0: # to avoid adding the first point since it's added when the spline is created.
2593 ob_spline_U.data.splines[0].bezier_points.add(1)
2594 ob_spline_U.data.splines[0].bezier_points[t].co = ob_curves_surf.data.splines[i].bezier_points[point_index].co
2597 if cyclic_loops_U[i] == True and not self.selection_U_exists: # If the loop is cyclic.
2598 # Add a last point at the same location as the first one.
2599 ob_spline_U.data.splines[0].bezier_points.add(1)
2600 ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = ob_spline_U.data.splines[0].bezier_points[0].co
2601 else:
2602 ob_spline_U.data.splines[0].use_cyclic_u = False
2605 splines_U_objects.append(ob_spline_U)
2608 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2609 bpy.data.objects[ob_spline_U.name].select = True
2610 bpy.context.scene.objects.active = bpy.data.objects[ob_spline_U.name]
2614 #### When option "Loops on strokes" is active each "Cross" loop will have its own proportions according to where the original strokes "touch" them.
2615 if self.loops_on_strokes:
2616 # Get the indices of points where the original strokes "touch" loops-U.
2617 points_U_crossed_by_strokes = []
2618 for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
2619 bp = splines_U_objects[0].data.splines[0].bezier_points[i]
2620 if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
2621 points_U_crossed_by_strokes.append(i)
2623 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke.
2624 edge_order_number_for_splines = {}
2625 if self.selection_V_exists:
2626 # For two-connected selections add a first hypothetic stroke at the begining.
2627 if selection_type == "TWO_CONNECTED":
2628 edge_order_number_for_splines[0] = 0
2631 for i in range(len(self.main_splines.data.splines)):
2632 sp = self.main_splines.data.splines[i]
2633 v_idx, dist_temp = self.shortest_distance(self.main_object, sp.bezier_points[0].co, verts_ordered_V_indices)
2635 edge_idx_in_chain = verts_ordered_V_indices.index(v_idx) # Get the position (edges count) of the vert v_idx in the selected chain V.
2637 # For two-connected selections the strokes go after the hypothetic stroke added before, so the index adds one per spline.
2638 if selection_type == "TWO_CONNECTED":
2639 spline_number = i + 1
2640 else:
2641 spline_number = i
2643 edge_order_number_for_splines[spline_number] = edge_idx_in_chain
2646 # Get the first and last verts indices for later comparison.
2647 if i == 0:
2648 first_v_idx = v_idx
2649 elif i == len(self.main_splines.data.splines) - 1:
2650 last_v_idx = v_idx
2653 if self.selection_V_is_closed:
2654 # If there is no last stroke on the last vertex (same as first vertex), add a hypothetic spline at last vert order.
2655 if first_v_idx != last_v_idx:
2656 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = len(verts_ordered_V_indices) - 1
2657 else:
2658 if self.cyclic_cross:
2659 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = len(verts_ordered_V_indices) - 2
2660 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = len(verts_ordered_V_indices) - 1
2661 else:
2662 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = len(verts_ordered_V_indices) - 1
2666 #### Get the coords of the points distributed along the "crossing curves", with appropriate proportions-V.
2667 surface_splines_parsed = []
2668 for i in range(len(splines_U_objects)):
2669 sp_ob = splines_U_objects[i]
2670 # If "Loops on strokes" option is active, calculate the proportions for each loop-U.
2671 if self.loops_on_strokes:
2672 # Segments distances from stroke to stroke.
2673 dist = 0
2674 full_dist = 0
2675 segments_distances = []
2676 for t in range(len(sp_ob.data.splines[0].bezier_points)):
2677 bp = sp_ob.data.splines[0].bezier_points[t]
2679 if t == 0:
2680 last_p = bp.co
2681 else:
2682 actual_p = bp.co
2683 dist += (last_p - actual_p).length
2685 if t in points_U_crossed_by_strokes:
2686 segments_distances.append(dist)
2687 full_dist += dist
2689 dist = 0
2691 last_p = actual_p
2693 # Calculate Proportions.
2694 used_edges_proportions_V = []
2695 for t in range(len(segments_distances)):
2696 if self.selection_V_exists:
2697 if t == 0:
2698 order_number_last_stroke = 0
2700 segment_edges_length_V = 0
2701 segment_edges_length_V2 = 0
2702 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2703 segment_edges_length_V += edges_lengths_V[order]
2704 if self.selection_V2_exists:
2705 segment_edges_length_V2 += edges_lengths_V2[order]
2708 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2709 # Calculate each "sub-segment" (the ones between each stroke) length.
2710 if self.selection_V2_exists:
2711 proportion_sub_seg = (edges_lengths_V2[order] - ((edges_lengths_V2[order] - edges_lengths_V[order]) / len(splines_U_objects) * i)) / (segment_edges_length_V2 - (segment_edges_length_V2 - segment_edges_length_V) / len(splines_U_objects) * i)
2712 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2713 else:
2714 proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
2715 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2717 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2719 order_number_last_stroke = edge_order_number_for_splines[t + 1]
2721 else:
2722 for c in range(self.edges_V):
2723 # Calculate each "sub-segment" (the ones between each stroke) length.
2724 sub_seg_dist = segments_distances[t] / self.edges_V
2725 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2727 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2728 surface_splines_parsed.append(actual_spline[0])
2730 else:
2731 if self.selection_V2_exists:
2732 used_edges_proportions_V = []
2733 for p in range(len(edges_proportions_V)):
2734 used_edges_proportions_V.append(edges_proportions_V2[p] - ((edges_proportions_V2[p] - edges_proportions_V[p]) / len(splines_U_objects) * i))
2735 else:
2736 used_edges_proportions_V = edges_proportions_V
2738 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2739 surface_splines_parsed.append(actual_spline[0])
2744 # Set the verts of the first and last splines to the locations of the respective verts in the selections.
2745 if self.selection_V_exists:
2746 for i in range(0, len(surface_splines_parsed[0])):
2747 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = self.main_object.matrix_world * verts_ordered_V[i].co
2749 if selection_type == "TWO_NOT_CONNECTED":
2750 if self.selection_V2_exists:
2751 for i in range(0, len(surface_splines_parsed[0])):
2752 surface_splines_parsed[0][i] = self.main_object.matrix_world * verts_ordered_V2[i].co
2757 # When "Automatic join" option is active (and the selection type is not "TWO_CONNECTED"), merge the verts of the tips of the loops when they are "near enough".
2758 if self.automatic_join and selection_type != "TWO_CONNECTED":
2759 #### Join the tips of "Follow" loops that are near enough and must be "closed".
2760 if not self.selection_V_exists and len(edges_proportions_U) >= 3:
2761 for i in range(len(surface_splines_parsed[0])):
2762 sp = surface_splines_parsed
2763 loop_segment_dist = (sp[0][i] - sp[1][i]).length
2764 full_loop_dist = loop_segment_dist * self.edges_U
2766 verts_middle_position_co = [(sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2, (sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2, (sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2]
2768 points_original = []
2769 points_original.append(sp[1][i])
2770 points_original.append(sp[0][i])
2772 points_target = []
2773 points_target.append(sp[1][i])
2774 points_target.append(mathutils.Vector(verts_middle_position_co))
2776 vec_A = points_original[0] - points_original[1]
2777 vec_B = points_target[0] - points_target[1]
2780 angle = vec_A.angle(vec_B) / math.pi
2782 edge_new_length = (mathutils.Vector(verts_middle_position_co) - sp[1][i]).length
2784 if edge_new_length <= loop_segment_dist * 1.5 * self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor: # If after moving the verts to the middle point, the segment doesn't stretch too much.
2785 if not (self.selection_U_exists and i == 0) and not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1): # Avoid joining when the actual loop must be merged with the original mesh.
2786 # Change the coords of both verts to the middle position.
2787 surface_splines_parsed[0][i] = verts_middle_position_co
2788 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
2792 #### Delete object with control points and object from grease pencil convertion.
2793 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2794 bpy.data.objects[ob_ctrl_pts.name].select = True
2795 bpy.context.scene.objects.active = bpy.data.objects[ob_ctrl_pts.name]
2797 bpy.ops.object.delete()
2800 for sp_ob in splines_U_objects:
2801 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2802 bpy.data.objects[sp_ob.name].select = True
2803 bpy.context.scene.objects.active = bpy.data.objects[sp_ob.name]
2805 bpy.ops.object.delete()
2810 #### Generate surface.
2812 # Get all verts coords.
2813 all_surface_verts_co = []
2814 for i in range(0, len(surface_splines_parsed)):
2815 # Get coords of all verts and make a list with them
2816 for pt_co in surface_splines_parsed[i]:
2817 all_surface_verts_co.append(pt_co)
2820 # Define verts for each face.
2821 all_surface_faces = []
2822 for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
2823 if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
2824 all_surface_faces.append([i+1, i , i + len(surface_splines_parsed[0]), i + len(surface_splines_parsed[0]) + 1])
2827 # Build the mesh.
2828 surf_me_name = "SURFSKIO_surface"
2829 me_surf = bpy.data.meshes.new(surf_me_name)
2831 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
2833 me_surf.update()
2835 ob_surface = bpy.data.objects.new(surf_me_name, me_surf)
2836 bpy.context.scene.objects.link(ob_surface)
2839 # Select all the "unselected but participating" verts, from closed selection or double selections with middle-vertex, for later join with remove doubles.
2840 for v_idx in single_unselected_verts:
2841 self.main_object.data.vertices[v_idx].select = True
2844 #### Join the new mesh to the main object.
2845 ob_surface.select = True
2846 self.main_object.select = True
2847 bpy.context.scene.objects.active = bpy.data.objects[self.main_object.name]
2849 bpy.ops.object.join('INVOKE_REGION_WIN')
2851 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2853 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
2854 bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
2855 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
2859 return{'FINISHED'}
2863 def execute(self, context):
2864 self.initial_global_undo_state = bpy.context.user_preferences.edit.use_global_undo
2866 bpy.context.user_preferences.edit.use_global_undo = False
2868 if not self.is_fill_faces:
2869 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode', value='True, False, False')
2871 # Build splines from the "last saved splines".
2872 last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
2873 self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
2874 bpy.context.scene.objects.link(self.main_splines)
2876 last_saved_curve.dimensions = "3D"
2878 for sp in self.last_strokes_splines_coords:
2879 spline = self.main_splines.data.splines.new('BEZIER')
2880 spline.bezier_points.add(len(sp) - 1) # less one because one point is added when the spline is created.
2881 for p in range(0, len(sp)):
2882 spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
2885 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2887 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2888 bpy.data.objects[self.main_splines.name].select = True
2889 bpy.context.scene.objects.active = bpy.data.objects[self.main_splines.name]
2891 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2893 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
2894 bpy.ops.curve.handle_type_set(type='VECTOR') # Important to make it vector first and then automatic, otherwise the tips handles get too big and distort the shrinkwrap results later.
2895 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2896 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
2897 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2900 self.main_splines.name = "SURFSKIO_temp_strokes"
2903 if self.is_crosshatch:
2904 strokes_for_crosshatch = True
2905 strokes_for_rectangular_surface = False
2906 else:
2907 strokes_for_rectangular_surface = True
2908 strokes_for_crosshatch = False
2911 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2912 bpy.data.objects[self.main_object.name].select = True
2913 bpy.context.scene.objects.active = bpy.data.objects[self.main_object.name]
2915 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2918 if strokes_for_rectangular_surface:
2919 self.rectangular_surface()
2920 elif strokes_for_crosshatch:
2921 self.crosshatch_surface_execute()
2924 #### Delete main splines
2925 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2927 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2928 bpy.data.objects[self.main_splines.name].select = True
2929 bpy.context.scene.objects.active = bpy.data.objects[self.main_splines.name]
2931 bpy.ops.object.delete()
2933 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2934 bpy.data.objects[self.main_object.name].select = True
2935 bpy.context.scene.objects.active = bpy.data.objects[self.main_object.name]
2937 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2940 bpy.context.user_preferences.edit.use_global_undo = self.initial_global_undo_state
2942 return{'FINISHED'}
2946 def invoke(self, context, event):
2947 self.initial_global_undo_state = bpy.context.user_preferences.edit.use_global_undo
2949 self.main_object = bpy.context.scene.objects.active
2950 self.main_object_selected_verts_count = int(self.main_object.data.total_vert_sel)
2953 bpy.context.user_preferences.edit.use_global_undo = False
2956 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode', value='True, False, False')
2958 # Out Edit mode and In again to make sure the actual mesh selections are being taken.
2959 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2960 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2964 self.cyclic_cross = bpy.context.scene.SURFSK_cyclic_cross
2965 self.cyclic_follow = bpy.context.scene.SURFSK_cyclic_follow
2966 self.automatic_join = bpy.context.scene.SURFSK_automatic_join
2967 self.loops_on_strokes = bpy.context.scene.SURFSK_loops_on_strokes
2968 self.keep_strokes = bpy.context.scene.SURFSK_keep_strokes
2970 self.edges_U = 10
2972 if self.loops_on_strokes:
2973 self.edges_V = 3
2974 else:
2975 self.edges_V = 10
2977 self.is_fill_faces = False
2979 self.stopping_errors = False
2981 self.last_strokes_splines_coords = []
2984 #### Determine the type of the strokes.
2985 self.strokes_type = get_strokes_type(self.main_object)
2987 #### Check if it will be used grease pencil strokes or curves.
2988 if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE": # If there are strokes to be used.
2989 if self.strokes_type == "GP_STROKES":
2990 # Convert grease pencil strokes to curve.
2991 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2992 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
2993 # XXX gpencil.convert now keep org object as active/selected, *not* newly created curve!
2994 # XXX This is far from perfect, but should work in most cases...
2995 # self.original_curve = bpy.context.object
2996 for ob in bpy.context.selected_objects:
2997 if ob != bpy.context.scene.objects.active and ob.name.startswith("GP_Layer"):
2998 self.original_curve = ob
2999 self.using_external_curves = False
3000 elif self.strokes_type == "EXTERNAL_CURVE":
3001 for ob in bpy.context.selected_objects:
3002 if ob != bpy.context.scene.objects.active:
3003 self.original_curve = ob
3004 self.using_external_curves = True
3006 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3009 #### Make sure there are no objects left from erroneous executions of this operator, with the reserved names used here.
3010 for o in bpy.data.objects:
3011 if o.name.find("SURFSKIO_") != -1:
3012 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3013 bpy.data.objects[o.name].select = True
3014 bpy.context.scene.objects.active = bpy.data.objects[o.name]
3016 bpy.ops.object.delete()
3019 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3020 bpy.data.objects[self.original_curve.name].select = True
3021 bpy.context.scene.objects.active = bpy.data.objects[self.original_curve.name]
3023 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3026 self.temporary_curve = bpy.context.scene.objects.active
3029 # Deselect all points of the curve
3030 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3031 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3032 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3036 # Delete splines with only a single isolated point.
3037 for i in range(len(self.temporary_curve.data.splines)):
3038 sp = self.temporary_curve.data.splines[i]
3040 if len(sp.bezier_points) == 1:
3041 sp.bezier_points[0].select_control_point = True
3043 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3044 bpy.ops.curve.delete(type='VERT')
3045 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3048 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3049 bpy.data.objects[self.temporary_curve.name].select = True
3050 bpy.context.scene.objects.active = bpy.data.objects[self.temporary_curve.name]
3052 #### Set a minimum number of points for crosshatch
3053 minimum_points_num = 15
3055 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3056 # Check if the number of points of each curve has at least the number of points of minimum_points_num, which is a bit more than the face-loops limit. If not, subdivide to reach at least that number of ponts.
3057 for i in range(len(self.temporary_curve.data.splines)):
3058 sp = self.temporary_curve.data.splines[i]
3060 if len(sp.bezier_points) < minimum_points_num:
3061 for bp in sp.bezier_points:
3062 bp.select_control_point = True
3064 if (len(sp.bezier_points) - 1) != 0:
3065 subdivide_cuts = int((minimum_points_num - len(sp.bezier_points)) / (len(sp.bezier_points) - 1)) + 1 # Formula to get the number of cuts that will make a curve of N number of points have near to "minimum_points_num" points, when subdividing with this number of cuts.
3066 else:
3067 subdivide_cuts = 0
3070 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts = subdivide_cuts)
3071 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3073 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3077 # Detect if the strokes are a crosshatch and do it if it is.
3078 self.crosshatch_surface_invoke(self.temporary_curve)
3082 if not self.is_crosshatch:
3083 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3084 bpy.data.objects[self.temporary_curve.name].select = True
3085 bpy.context.scene.objects.active = bpy.data.objects[self.temporary_curve.name]
3087 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3089 #### Set a minimum number of points for rectangular surfaces.
3090 minimum_points_num = 60
3092 # Check if the number of points of each curve has at least the number of points of minimum_points_num, which is a bit more than the face-loops limit. If not, subdivide to reach at least that number of ponts.
3093 for i in range(len(self.temporary_curve.data.splines)):
3094 sp = self.temporary_curve.data.splines[i]
3096 if len(sp.bezier_points) < minimum_points_num:
3097 for bp in sp.bezier_points:
3098 bp.select_control_point = True
3100 if (len(sp.bezier_points) - 1) != 0:
3101 subdivide_cuts = int((minimum_points_num - len(sp.bezier_points)) / (len(sp.bezier_points) - 1)) + 1 # Formula to get the number of cuts that will make a curve of N number of points have near to "minimum_points_num" points, when subdividing with this number of cuts.
3102 else:
3103 subdivide_cuts = 0
3106 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts = subdivide_cuts)
3107 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3109 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3114 # Save coordinates of the actual strokes (as the "last saved splines").
3115 for sp_idx in range(len(self.temporary_curve.data.splines)):
3116 self.last_strokes_splines_coords.append([])
3117 for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
3118 coords = self.temporary_curve.matrix_world * self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
3119 self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
3122 # Check for cyclic splines, put the first and last points in the middle of their actual positions.
3123 for sp_idx in range(len(self.temporary_curve.data.splines)):
3124 if self.temporary_curve.data.splines[sp_idx].use_cyclic_u == True:
3125 first_p_co = self.last_strokes_splines_coords[sp_idx][0]
3126 last_p_co = self.last_strokes_splines_coords[sp_idx][len(self.last_strokes_splines_coords[sp_idx]) - 1]
3128 target_co = [(first_p_co[0] + last_p_co[0]) / 2, (first_p_co[1] + last_p_co[1]) / 2, (first_p_co[2] + last_p_co[2]) / 2]
3130 self.last_strokes_splines_coords[sp_idx][0] = target_co
3131 self.last_strokes_splines_coords[sp_idx][len(self.last_strokes_splines_coords[sp_idx]) - 1] = target_co
3133 tuple(self.last_strokes_splines_coords)
3137 # Estimation of the average length of the segments between each point of the grease pencil strokes. Will be useful to determine whether a curve should be made "Cyclic".
3138 segments_lengths_sum = 0
3139 segments_count = 0
3140 random_spline = self.temporary_curve.data.splines[0].bezier_points
3141 for i in range(0, len(random_spline)):
3142 if i != 0 and len(random_spline) - 1 >= i:
3143 segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
3144 segments_count += 1
3146 self.average_gp_segment_length = segments_lengths_sum / segments_count
3149 #### Delete temporary strokes curve object
3150 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3151 bpy.data.objects[self.temporary_curve.name].select = True
3152 bpy.context.scene.objects.active = bpy.data.objects[self.temporary_curve.name]
3154 bpy.ops.object.delete()
3157 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3158 bpy.data.objects[self.main_object.name].select = True
3159 bpy.context.scene.objects.active = bpy.data.objects[self.main_object.name]
3161 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3164 self.execute(context)
3165 bpy.context.user_preferences.edit.use_global_undo = False # Set again since "execute()" will turn it again to its initial value.
3168 #### If "Keep strokes" option is not active, delete original strokes curve object.
3169 if (not self.stopping_errors and not self.keep_strokes) or self.is_crosshatch:
3170 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3171 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3172 bpy.data.objects[self.original_curve.name].select = True
3173 bpy.context.scene.objects.active = bpy.data.objects[self.original_curve.name]
3175 bpy.ops.object.delete()
3177 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3178 bpy.data.objects[self.main_object.name].select = True
3179 bpy.context.scene.objects.active = bpy.data.objects[self.main_object.name]
3181 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3185 #### Delete grease pencil strokes.
3186 if self.strokes_type == "GP_STROKES" and not self.stopping_errors:
3187 bpy.ops.gpencil.active_frame_delete('INVOKE_REGION_WIN')
3190 bpy.context.user_preferences.edit.use_global_undo = self.initial_global_undo_state
3193 if not self.stopping_errors:
3194 return {"FINISHED"}
3195 else:
3196 return{"CANCELLED"}
3198 elif self.strokes_type == "SELECTION_ALONE":
3199 self.is_fill_faces = True
3201 created_faces_count = self.fill_with_faces(self.main_object)
3203 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3205 bpy.context.user_preferences.edit.use_global_undo = self.initial_global_undo_state
3207 if created_faces_count == 0:
3208 self.report({'WARNING'}, "There aren't any strokes attatched to the object")
3209 return {"CANCELLED"}
3210 else:
3211 return {"FINISHED"}
3214 bpy.context.user_preferences.edit.use_global_undo = self.initial_global_undo_state
3216 if self.strokes_type == "EXTERNAL_NO_CURVE":
3217 self.report({'WARNING'}, "The secondary object is not a Curve.")
3218 return{"CANCELLED"}
3220 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3221 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3222 return{"CANCELLED"}
3224 elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3225 self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3226 return{"CANCELLED"}
3228 elif self.strokes_type == "NO_STROKES":
3229 self.report({'WARNING'}, "There aren't any strokes attatched to the object")
3230 return{"CANCELLED"}
3232 elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
3233 self.report({'WARNING'}, "All splines must be Bezier.")
3234 return{"CANCELLED"}
3236 else:
3237 return{"CANCELLED"}
3240 # Edit strokes operator.
3241 class GPENCIL_OT_SURFSK_edit_strokes(bpy.types.Operator):
3242 bl_idname = "gpencil.surfsk_edit_strokes"
3243 bl_label = "Bsurfaces edit strokes"
3244 bl_description = "Edit the grease pencil strokes or curves used"
3247 def execute(self, context):
3248 #### Determine the type of the strokes.
3249 self.strokes_type = get_strokes_type(self.main_object)
3250 #### Check if strokes are grease pencil strokes or a curves object.
3251 selected_objs = bpy.context.selected_objects
3252 if self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3253 for ob in selected_objs:
3254 if ob != bpy.context.scene.objects.active:
3255 curve_ob = ob
3257 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3259 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3260 bpy.data.objects[curve_ob.name].select = True
3261 bpy.context.scene.objects.active = bpy.data.objects[curve_ob.name]
3263 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3264 elif self.strokes_type == "GP_STROKES" or self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION":
3265 #### Convert grease pencil strokes to curve.
3266 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3267 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3268 for ob in bpy.context.selected_objects:
3269 if ob != bpy.context.scene.objects.active and ob.name.startswith("GP_Layer"):
3270 ob_gp_strokes = ob
3272 #ob_gp_strokes = bpy.context.object
3274 #### Delete grease pencil strokes.
3275 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3276 bpy.data.objects[self.main_object.name].select = True
3277 bpy.context.scene.objects.active = bpy.data.objects[self.main_object.name]
3279 bpy.ops.gpencil.active_frame_delete('INVOKE_REGION_WIN')
3282 #### Clean up curves.
3283 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3284 bpy.data.objects[ob_gp_strokes.name].select = True
3285 bpy.context.scene.objects.active = bpy.data.objects[ob_gp_strokes.name]
3287 curve_crv = ob_gp_strokes.data
3288 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3289 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type="BEZIER")
3290 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type="AUTOMATIC")
3291 bpy.data.curves[curve_crv.name].show_handles = False
3292 bpy.data.curves[curve_crv.name].show_normal_face = False
3294 elif self.strokes_type == "EXTERNAL_NO_CURVE":
3295 self.report({'WARNING'}, "The secondary object is not a Curve.")
3296 return{"CANCELLED"}
3297 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3298 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3299 return{"CANCELLED"}
3300 elif self.strokes_type == "NO_STROKES" or self.strokes_type == "SELECTION_ALONE":
3301 self.report({'WARNING'}, "There aren't any strokes attatched to the object")
3302 return{"CANCELLED"}
3303 else:
3304 return{"CANCELLED"}
3308 def invoke (self, context, event):
3309 self.main_object = bpy.context.object
3311 self.execute(context)
3313 return {"FINISHED"}
3316 class CURVE_OT_SURFSK_reorder_splines(bpy.types.Operator):
3317 bl_idname = "curve.surfsk_reorder_splines"
3318 bl_label = "Bsurfaces reorder splines"
3319 bl_description = "Defines the order of the splines by using grease pencil strokes"
3320 bl_options = {'REGISTER', 'UNDO'}
3322 def execute(self, context):
3323 objects_to_delete = []
3324 #### Convert grease pencil strokes to curve.
3325 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3326 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3327 for ob in bpy.context.selected_objects:
3328 if ob != bpy.context.scene.objects.active and ob.name.startswith("GP_Layer"):
3329 GP_strokes_curve = ob
3331 #GP_strokes_curve = bpy.context.object
3332 objects_to_delete.append(GP_strokes_curve)
3334 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3335 bpy.data.objects[GP_strokes_curve.name].select = True
3336 bpy.context.scene.objects.active = bpy.data.objects[GP_strokes_curve.name]
3339 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3340 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3341 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts = 100)
3342 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3344 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3345 GP_strokes_mesh = bpy.context.object
3346 objects_to_delete.append(GP_strokes_mesh)
3348 GP_strokes_mesh.data.resolution_u = 1
3349 bpy.ops.object.convert(target='MESH', keep_original=False)
3352 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3353 bpy.data.objects[self.main_curve.name].select = True
3354 bpy.context.scene.objects.active = bpy.data.objects[self.main_curve.name]
3356 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3357 curves_duplicate_1 = bpy.context.object
3358 objects_to_delete.append(curves_duplicate_1)
3362 minimum_points_num = 500
3365 for x in range(round(minimum_points_num / 100)): # Some iterations since the subdivision operator has a limit of 100 subdivisions per iteration.
3366 #### Check if the number of points of each curve has at least the number of points of minimum_points_num. If not, subdivide to reach at least that number of ponts.
3367 for i in range(len(curves_duplicate_1.data.splines)):
3368 sp = curves_duplicate_1.data.splines[i]
3370 if len(sp.bezier_points) < minimum_points_num:
3371 for bp in sp.bezier_points:
3372 bp.select_control_point = True
3374 if (len(sp.bezier_points) - 1) != 0:
3375 subdivide_cuts = int((minimum_points_num - len(sp.bezier_points)) / (len(sp.bezier_points) - 1)) + 1 # Formula to get the number of cuts that will make a curve of N number of points have near to "minimum_points_num" points, when subdividing with this number of cuts.
3376 else:
3377 subdivide_cuts = 0
3379 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3380 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts = subdivide_cuts)
3381 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3382 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3385 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3386 curves_duplicate_2 = bpy.context.object
3387 objects_to_delete.append(curves_duplicate_2)
3390 #### Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target.
3391 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3392 bpy.data.objects[curves_duplicate_2.name].select = True
3393 bpy.context.scene.objects.active = bpy.data.objects[curves_duplicate_2.name]
3395 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
3396 curves_duplicate_2.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX"
3397 curves_duplicate_2.modifiers["Shrinkwrap"].target = GP_strokes_mesh
3398 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier='Shrinkwrap')
3401 #### Get the distance of each vert from its original position to its position with Shrinkwrap.
3402 nearest_points_coords = {}
3403 for st_idx in range(len(curves_duplicate_1.data.splines)):
3404 for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
3405 bp_1_co = curves_duplicate_1.matrix_world * curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
3406 bp_2_co = curves_duplicate_2.matrix_world * curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
3408 if bp_idx == 0:
3409 shortest_dist = (bp_1_co - bp_2_co).length
3410 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0], "%.4f" % bp_2_co[1], "%.4f" % bp_2_co[2])
3412 dist = (bp_1_co - bp_2_co).length
3414 if dist < shortest_dist:
3415 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0], "%.4f" % bp_2_co[1], "%.4f" % bp_2_co[2])
3416 shortest_dist = dist
3420 #### Get all coords of GP strokes points, for comparison.
3421 GP_strokes_coords = []
3422 for st_idx in range(len(GP_strokes_curve.data.splines)):
3423 GP_strokes_coords.append([("%.4f" % x if "%.4f" % x != "-0.00" else "0.00", "%.4f" % y if "%.4f" % y != "-0.00" else "0.00", "%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]])
3426 #### Check the point of the GP strokes with the same coords as the nearest points of the curves (with shrinkwrap).
3427 GP_connection_points = {} # Dictionary with GP stroke index as index, and a list as value. The list has as index the point index of the GP stroke nearest to the spline, and as value the spline index.
3428 for gp_st_idx in range(len(GP_strokes_coords)):
3429 GPvert_spline_relationship = {}
3431 for splines_st_idx in range(len(nearest_points_coords)):
3432 if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
3433 GPvert_spline_relationship[GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])] = splines_st_idx
3436 GP_connection_points[gp_st_idx] = GPvert_spline_relationship
3439 #### Get the splines new order.
3440 splines_new_order = []
3441 for i in GP_connection_points:
3442 dict_keys = sorted(GP_connection_points[i].keys()) # Sort dictionaries by key
3444 for k in dict_keys:
3445 splines_new_order.append(GP_connection_points[i][k])
3449 #### Reorder.
3451 curve_original_name = self.main_curve.name
3453 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3454 bpy.data.objects[self.main_curve.name].select = True
3455 bpy.context.scene.objects.active = bpy.data.objects[self.main_curve.name]
3457 self.main_curve.name = "SURFSKIO_CRV_ORD"
3459 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3460 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3461 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3464 for sp_idx in range(len(self.main_curve.data.splines)):
3465 self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
3467 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3468 bpy.ops.curve.separate('EXEC_REGION_WIN')
3469 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3473 #### Get the names of the separated splines objects in the original order.
3474 splines_unordered = {}
3475 for o in bpy.data.objects:
3476 if o.name.find("SURFSKIO_CRV_ORD") != -1:
3477 spline_order_string = o.name.partition(".")[2]
3479 if spline_order_string != "" and int(spline_order_string) > 0:
3480 spline_order_index = int(spline_order_string) - 1
3481 splines_unordered[spline_order_index] = o.name
3485 #### Join all splines objects in final order.
3486 for order_idx in splines_new_order:
3487 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3488 bpy.data.objects[splines_unordered[order_idx]].select = True
3489 bpy.data.objects["SURFSKIO_CRV_ORD"].select = True
3490 bpy.context.scene.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
3492 bpy.ops.object.join('INVOKE_REGION_WIN')
3495 #### Go back to the original name of the curves object.
3496 bpy.context.object.name = curve_original_name
3499 #### Delete all unused objects.
3500 for o in objects_to_delete:
3501 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3502 bpy.data.objects[o.name].select = True
3503 bpy.context.scene.objects.active = bpy.data.objects[o.name]
3505 bpy.ops.object.delete()
3508 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3509 bpy.data.objects[curve_original_name].select = True
3510 bpy.context.scene.objects.active = bpy.data.objects[curve_original_name]
3512 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3513 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3516 bpy.ops.gpencil.active_frame_delete('INVOKE_REGION_WIN')
3520 return {"FINISHED"}
3524 def invoke (self, context, event):
3525 self.main_curve = bpy.context.object
3528 there_are_GP_strokes = False
3529 try:
3530 #### Get the active grease pencil layer.
3531 strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
3533 if strokes_num > 0:
3534 there_are_GP_strokes = True
3535 except:
3536 pass
3539 if there_are_GP_strokes:
3540 self.execute(context)
3541 self.report({'INFO'}, "Splines have been reordered.")
3542 else:
3543 self.report({'WARNING'}, "Draw grease pencil strokes to connect splines.")
3545 return {"FINISHED"}
3550 class CURVE_OT_SURFSK_first_points(bpy.types.Operator):
3551 bl_idname = "curve.surfsk_first_points"
3552 bl_label = "Bsurfaces set first points"
3553 bl_description = "Set the selected points as the first point of each spline"
3554 bl_options = {'REGISTER', 'UNDO'}
3558 def execute(self, context):
3559 splines_to_invert = []
3561 #### Check non-cyclic splines to invert.
3562 for i in range(len(self.main_curve.data.splines)):
3563 b_points = self.main_curve.data.splines[i].bezier_points
3565 if not i in self.cyclic_splines: # Only for non-cyclic splines
3566 if b_points[len(b_points) - 1].select_control_point:
3567 splines_to_invert.append(i)
3570 #### Reorder points of cyclic splines, and set all handles to "Automatic".
3572 # Check first selected point.
3573 cyclic_splines_new_first_pt = {}
3574 for i in self.cyclic_splines:
3575 sp = self.main_curve.data.splines[i]
3577 for t in range(len(sp.bezier_points)):
3578 bp = sp.bezier_points[t]
3579 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
3580 cyclic_splines_new_first_pt[i] = t
3581 break # To take only one if there are more.
3583 # Reorder.
3584 for spline_idx in cyclic_splines_new_first_pt:
3585 sp = self.main_curve.data.splines[spline_idx]
3587 spline_old_coords = []
3588 for bp_old in sp.bezier_points:
3589 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
3591 left_handle_type = str(bp_old.handle_left_type)
3592 left_handle_length = float(bp_old.handle_left.length)
3593 left_handle_xyz = (float(bp_old.handle_left.x), float(bp_old.handle_left.y), float(bp_old.handle_left.z))
3595 right_handle_type = str(bp_old.handle_right_type)
3596 right_handle_length = float(bp_old.handle_right.length)
3597 right_handle_xyz = (float(bp_old.handle_right.x), float(bp_old.handle_right.y), float(bp_old.handle_right.z))
3599 spline_old_coords.append([coords, left_handle_type, right_handle_type, left_handle_length, right_handle_length, left_handle_xyz, right_handle_xyz])
3602 for t in range(len(sp.bezier_points)):
3603 bp = sp.bezier_points
3605 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
3606 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
3607 else:
3608 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
3610 bp[t].co = mathutils.Vector(spline_old_coords[new_index][0])
3612 bp[t].handle_left.length = spline_old_coords[new_index][3]
3613 bp[t].handle_right.length = spline_old_coords[new_index][4]
3615 bp[t].handle_left_type = "FREE"
3616 bp[t].handle_right_type = "FREE"
3618 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
3619 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
3620 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
3622 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
3623 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
3624 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
3626 bp[t].handle_left_type = spline_old_coords[new_index][1]
3627 bp[t].handle_right_type = spline_old_coords[new_index][2]
3631 #### Invert the non-cyclic splines designated above.
3632 for i in range(len(splines_to_invert)):
3633 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3635 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3636 self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
3637 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3639 bpy.ops.curve.switch_direction()
3641 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3644 #### Keep selected the first vert of each spline.
3645 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3646 for i in range(len(self.main_curve.data.splines)):
3647 if not self.main_curve.data.splines[i].use_cyclic_u:
3648 bp = self.main_curve.data.splines[i].bezier_points[0]
3649 else:
3650 bp = self.main_curve.data.splines[i].bezier_points[len(self.main_curve.data.splines[i].bezier_points) - 1]
3652 bp.select_control_point = True
3653 bp.select_right_handle = True
3654 bp.select_left_handle = True
3655 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3660 return {'FINISHED'}
3664 def invoke (self, context, event):
3665 self.main_curve = bpy.context.object
3667 # Check if all curves are Bezier, and detect which ones are cyclic.
3668 self.cyclic_splines = []
3669 for i in range(len(self.main_curve.data.splines)):
3670 if self.main_curve.data.splines[i].type != "BEZIER":
3671 self.report({'WARNING'}, 'All splines must be Bezier type.')
3673 return {'CANCELLED'}
3674 else:
3675 if self.main_curve.data.splines[i].use_cyclic_u:
3676 self.cyclic_splines.append(i)
3680 self.execute(context)
3681 self.report({'INFO'}, "First points have been set.")
3683 return {'FINISHED'}
3688 def register():
3689 bpy.utils.register_class(VIEW3D_PT_tools_SURFSK_mesh)
3690 bpy.utils.register_class(VIEW3D_PT_tools_SURFSK_curve)
3691 bpy.utils.register_class(GPENCIL_OT_SURFSK_add_surface)
3692 bpy.utils.register_class(GPENCIL_OT_SURFSK_edit_strokes)
3693 bpy.utils.register_class(CURVE_OT_SURFSK_reorder_splines)
3694 bpy.utils.register_class(CURVE_OT_SURFSK_first_points)
3698 bpy.types.Scene.SURFSK_cyclic_cross = bpy.props.BoolProperty(
3699 name="Cyclic Cross",
3700 description="Make cyclic the face-loops crossing the strokes",
3701 default=False)
3703 bpy.types.Scene.SURFSK_cyclic_follow = bpy.props.BoolProperty(
3704 name="Cyclic Follow",
3705 description="Make cyclic the face-loops following the strokes",
3706 default=False)
3708 bpy.types.Scene.SURFSK_keep_strokes = bpy.props.BoolProperty(
3709 name="Keep strokes",
3710 description="Keeps the sketched strokes or curves after adding the surface",
3711 default=False)
3713 bpy.types.Scene.SURFSK_automatic_join = bpy.props.BoolProperty(
3714 name="Automatic join",
3715 description="Join automatically vertices of either surfaces generated by crosshatching, or from the borders of closed shapes",
3716 default=True)
3718 bpy.types.Scene.SURFSK_loops_on_strokes = bpy.props.BoolProperty(
3719 name="Loops on strokes",
3720 description="Make the loops match the paths of the strokes",
3721 default=True)
3723 bpy.types.Scene.SURFSK_precision = bpy.props.IntProperty(
3724 name="Precision",
3725 description="Precision level of the surface calculation",
3726 default=2,
3727 min=1,
3728 max=100)
3731 def unregister():
3732 bpy.utils.unregister_class(VIEW3D_PT_tools_SURFSK_mesh)
3733 bpy.utils.unregister_class(VIEW3D_PT_tools_SURFSK_curve)
3734 bpy.utils.unregister_class(GPENCIL_OT_SURFSK_add_surface)
3735 bpy.utils.unregister_class(GPENCIL_OT_SURFSK_edit_strokes)
3736 bpy.utils.unregister_class(CURVE_OT_SURFSK_reorder_splines)
3737 bpy.utils.unregister_class(CURVE_OT_SURFSK_first_points)
3739 del bpy.types.Scene.SURFSK_precision
3740 del bpy.types.Scene.SURFSK_keep_strokes
3741 del bpy.types.Scene.SURFSK_automatic_join
3742 del bpy.types.Scene.SURFSK_cyclic_cross
3743 del bpy.types.Scene.SURFSK_cyclic_follow
3744 del bpy.types.Scene.SURFSK_loops_on_strokes
3748 if __name__ == "__main__":
3749 register()