Node Wrangler: another update for 2.8
[blender-addons.git] / mesh_bsurfaces.py
blobc0c1e994142936db469ef837b0eea4f117ea1564
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, 1),
24 "blender": (2, 76, 0),
25 "location": "View3D > EditMode > ToolShelf",
26 "description": "Modeling and retopology tool",
27 "wiki_url": "https://wiki.blender.org/index.php/Dev:Ref/Release_Notes/2.64/Bsurfaces_1.5",
28 "category": "Mesh",
32 import bpy
33 import bmesh
35 import operator
36 from mathutils import Vector
37 from mathutils.geometry import (
38 intersect_line_line,
39 intersect_point_line,
41 from math import (
42 degrees,
43 pi,
44 sqrt,
46 from bpy.props import (
47 BoolProperty,
48 FloatProperty,
49 IntProperty,
50 StringProperty,
51 PointerProperty,
53 from bpy.types import (
54 Operator,
55 Panel,
56 PropertyGroup,
57 AddonPreferences,
61 class VIEW3D_PT_tools_SURFSK_mesh(Panel):
62 bl_space_type = 'VIEW_3D'
63 bl_region_type = 'TOOLS'
64 bl_category = 'Tools'
65 bl_context = "mesh_edit"
66 bl_label = "Bsurfaces"
68 @classmethod
69 def poll(cls, context):
70 return context.active_object
72 def draw(self, context):
73 layout = self.layout
74 scn = context.scene.bsurfaces
76 col = layout.column(align=True)
77 row = layout.row()
78 row.separator()
79 col.operator("gpencil.surfsk_add_surface", text="Add Surface")
80 col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
81 col.prop(scn, "SURFSK_cyclic_cross")
82 col.prop(scn, "SURFSK_cyclic_follow")
83 col.prop(scn, "SURFSK_loops_on_strokes")
84 col.prop(scn, "SURFSK_automatic_join")
85 col.prop(scn, "SURFSK_keep_strokes")
88 class VIEW3D_PT_tools_SURFSK_curve(Panel):
89 bl_space_type = 'VIEW_3D'
90 bl_region_type = 'TOOLS'
91 bl_context = "curve_edit"
92 bl_category = 'Tools'
93 bl_label = "Bsurfaces"
95 @classmethod
96 def poll(cls, context):
97 return context.active_object
99 def draw(self, context):
100 layout = self.layout
102 col = layout.column(align=True)
103 row = layout.row()
104 row.separator()
105 col.operator("curve.surfsk_first_points", text="Set First Points")
106 col.operator("curve.switch_direction", text="Switch Direction")
107 col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
110 # Returns the type of strokes used
111 def get_strokes_type(main_object):
112 strokes_type = ""
113 strokes_num = 0
115 # Check if they are grease pencil
116 try:
117 # Get the active grease pencil layer
118 strokes_num = len(main_object.grease_pencil.layers.active.active_frame.strokes)
120 if strokes_num > 0:
121 strokes_type = "GP_STROKES"
122 except:
123 pass
125 # Check if they are curves, if there aren't grease pencil strokes
126 if strokes_type == "":
127 if len(bpy.context.selected_objects) == 2:
128 for ob in bpy.context.selected_objects:
129 if ob != bpy.context.scene.objects.active and ob.type == "CURVE":
130 strokes_type = "EXTERNAL_CURVE"
131 strokes_num = len(ob.data.splines)
133 # Check if there is any non-bezier spline
134 for i in range(len(ob.data.splines)):
135 if ob.data.splines[i].type != "BEZIER":
136 strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
137 break
139 elif ob != bpy.context.scene.objects.active and ob.type != "CURVE":
140 strokes_type = "EXTERNAL_NO_CURVE"
141 elif len(bpy.context.selected_objects) > 2:
142 strokes_type = "MORE_THAN_ONE_EXTERNAL"
144 # Check if there is a single stroke without any selection in the object
145 if strokes_num == 1 and main_object.data.total_vert_sel == 0:
146 if strokes_type == "EXTERNAL_CURVE":
147 strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
148 elif strokes_type == "GP_STROKES":
149 strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"
151 if strokes_num == 0 and main_object.data.total_vert_sel > 0:
152 strokes_type = "SELECTION_ALONE"
154 if strokes_type == "":
155 strokes_type = "NO_STROKES"
157 return strokes_type
160 # Surface generator operator
161 class GPENCIL_OT_SURFSK_add_surface(Operator):
162 bl_idname = "gpencil.surfsk_add_surface"
163 bl_label = "Bsurfaces add surface"
164 bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
165 bl_options = {'REGISTER', 'UNDO'}
167 edges_U = IntProperty(
168 name="Cross",
169 description="Number of face-loops crossing the strokes",
170 default=1,
171 min=1,
172 max=200
174 edges_V = IntProperty(
175 name="Follow",
176 description="Number of face-loops following the strokes",
177 default=1,
178 min=1,
179 max=200
181 cyclic_cross = BoolProperty(
182 name="Cyclic Cross",
183 description="Make cyclic the face-loops crossing the strokes",
184 default=False
186 cyclic_follow = BoolProperty(
187 name="Cyclic Follow",
188 description="Make cyclic the face-loops following the strokes",
189 default=False
191 loops_on_strokes = BoolProperty(
192 name="Loops on strokes",
193 description="Make the loops match the paths of the strokes",
194 default=False
196 automatic_join = BoolProperty(
197 name="Automatic join",
198 description="Join automatically vertices of either surfaces generated "
199 "by crosshatching, or from the borders of closed shapes",
200 default=False
202 join_stretch_factor = FloatProperty(
203 name="Stretch",
204 description="Amount of stretching or shrinking allowed for "
205 "edges when joining vertices automatically",
206 default=1,
207 min=0,
208 max=3,
209 subtype='FACTOR'
212 def draw(self, context):
213 layout = self.layout
214 col = layout.column(align=True)
215 row = layout.row()
217 if not self.is_fill_faces:
218 row.separator()
219 if not self.is_crosshatch:
220 if not self.selection_U_exists:
221 col.prop(self, "edges_U")
222 row.separator()
224 if not self.selection_V_exists:
225 col.prop(self, "edges_V")
226 row.separator()
228 row.separator()
230 if not self.selection_U_exists:
231 if not (
232 (self.selection_V_exists and not self.selection_V_is_closed) or
233 (self.selection_V2_exists and not self.selection_V2_is_closed)
235 col.prop(self, "cyclic_cross")
237 if not self.selection_V_exists:
238 if not (
239 (self.selection_U_exists and not self.selection_U_is_closed) or
240 (self.selection_U2_exists and not self.selection_U2_is_closed)
242 col.prop(self, "cyclic_follow")
244 col.prop(self, "loops_on_strokes")
246 col.prop(self, "automatic_join")
248 if self.automatic_join:
249 row.separator()
250 col.separator()
251 row.separator()
252 col.prop(self, "join_stretch_factor")
254 # Get an ordered list of a chain of vertices
255 def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx,
256 first_vert_idx, middle_vertex_idx, closing_vert_idx):
257 # Order selected vertices.
258 verts_ordered = []
259 if closing_vert_idx is not None:
260 verts_ordered.append(ob.data.vertices[closing_vert_idx])
262 verts_ordered.append(ob.data.vertices[first_vert_idx])
263 prev_v = first_vert_idx
264 prev_ed = None
265 finish_while = False
266 while True:
267 edges_non_matched = 0
268 for i in all_selected_edges_idx:
269 if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and \
270 ob.data.edges[i].vertices[1] in all_selected_verts_idx:
272 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
273 prev_v = ob.data.edges[i].vertices[1]
274 prev_ed = ob.data.edges[i]
275 elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and \
276 ob.data.edges[i].vertices[0] in all_selected_verts_idx:
278 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
279 prev_v = ob.data.edges[i].vertices[0]
280 prev_ed = ob.data.edges[i]
281 else:
282 edges_non_matched += 1
284 if edges_non_matched == len(all_selected_edges_idx):
285 finish_while = True
287 if finish_while:
288 break
290 if closing_vert_idx is not None:
291 verts_ordered.append(ob.data.vertices[closing_vert_idx])
293 if middle_vertex_idx is not None:
294 verts_ordered.append(ob.data.vertices[middle_vertex_idx])
295 verts_ordered.reverse()
297 return tuple(verts_ordered)
299 # Calculates length of a chain of points.
300 def get_chain_length(self, object, verts_ordered):
301 matrix = object.matrix_world
303 edges_lengths = []
304 edges_lengths_sum = 0
305 for i in range(0, len(verts_ordered)):
306 if i == 0:
307 prev_v_co = matrix * verts_ordered[i].co
308 else:
309 v_co = matrix * verts_ordered[i].co
311 v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
312 edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
314 edges_lengths.append(edge_length)
315 edges_lengths_sum += edge_length
317 prev_v_co = v_co
319 return edges_lengths, edges_lengths_sum
321 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
322 def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
323 edges_proportions = []
324 if use_boundaries:
325 verts_count = 1
326 for l in edges_lengths:
327 edges_proportions.append(l / edges_lengths_sum)
328 verts_count += 1
329 else:
330 verts_count = 1
331 for n in range(0, fixed_edges_num):
332 edges_proportions.append(1 / fixed_edges_num)
333 verts_count += 1
335 return edges_proportions
337 # Calculates the angle between two pairs of points in space
338 def orientation_difference(self, points_A_co, points_B_co):
339 # each parameter should be a list with two elements,
340 # and each element should be a x,y,z coordinate
341 vec_A = points_A_co[0] - points_A_co[1]
342 vec_B = points_B_co[0] - points_B_co[1]
344 angle = vec_A.angle(vec_B)
346 if angle > 0.5 * pi:
347 angle = abs(angle - pi)
349 return angle
351 # Calculate the which vert of verts_idx list is the nearest one
352 # to the point_co coordinates, and the distance
353 def shortest_distance(self, object, point_co, verts_idx):
354 matrix = object.matrix_world
356 for i in range(0, len(verts_idx)):
357 dist = (point_co - matrix * object.data.vertices[verts_idx[i]].co).length
358 if i == 0:
359 prev_dist = dist
360 nearest_vert_idx = verts_idx[i]
361 shortest_dist = dist
363 if dist < prev_dist:
364 prev_dist = dist
365 nearest_vert_idx = verts_idx[i]
366 shortest_dist = dist
368 return nearest_vert_idx, shortest_dist
370 # Returns the index of the opposite vert tip in a chain, given a vert tip index
371 # as parameter, and a multidimentional list with all pairs of tips
372 def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
373 opposite_vert_tip_idx = None
374 for i in range(0, len(all_chains_tips_idx)):
375 if vert_tip_idx == all_chains_tips_idx[i][0]:
376 opposite_vert_tip_idx = all_chains_tips_idx[i][1]
377 if vert_tip_idx == all_chains_tips_idx[i][1]:
378 opposite_vert_tip_idx = all_chains_tips_idx[i][0]
380 return opposite_vert_tip_idx
382 # Simplifies a spline and returns the new points coordinates
383 def simplify_spline(self, spline_coords, segments_num):
384 simplified_spline = []
385 points_between_segments = round(len(spline_coords) / segments_num)
387 simplified_spline.append(spline_coords[0])
388 for i in range(1, segments_num):
389 simplified_spline.append(spline_coords[i * points_between_segments])
391 simplified_spline.append(spline_coords[len(spline_coords) - 1])
393 return simplified_spline
395 # Cleans up the scene and gets it the same it was at the beginning,
396 # in case the script is interrupted in the middle of the execution
397 def cleanup_on_interruption(self):
398 # If the original strokes curve comes from conversion
399 # from grease pencil and wasn't made by hand, delete it
400 if not self.using_external_curves:
401 try:
402 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
403 self.original_curve.select = True
404 bpy.context.scene.objects.active = self.original_curve
406 bpy.ops.object.delete()
407 except:
408 pass
410 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
411 self.main_object.select = True
412 bpy.context.scene.objects.active = self.main_object
413 else:
414 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
415 self.original_curve.select = True
416 self.main_object.select = True
417 bpy.context.scene.objects.active = self.main_object
419 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
421 # Returns a list with the coords of the points distributed over the splines
422 # passed to this method according to the proportions parameter
423 def distribute_pts(self, surface_splines, proportions):
425 # Calculate the length of each final surface spline
426 surface_splines_lengths = []
427 surface_splines_parsed = []
429 for sp_idx in range(0, len(surface_splines)):
430 # Calculate spline length
431 surface_splines_lengths.append(0)
433 for i in range(0, len(surface_splines[sp_idx].bezier_points)):
434 if i == 0:
435 prev_p = surface_splines[sp_idx].bezier_points[i]
436 else:
437 p = surface_splines[sp_idx].bezier_points[i]
438 edge_length = (prev_p.co - p.co).length
439 surface_splines_lengths[sp_idx] += edge_length
441 prev_p = p
443 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
444 for sp_idx in range(0, len(surface_splines)):
445 surface_splines_parsed.append([])
446 surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
448 prev_p_co = surface_splines[sp_idx].bezier_points[0].co
449 p_idx = 0
451 for prop_idx in range(len(proportions) - 1):
452 target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
453 partial_segment_length = 0
454 finish_while = False
456 while True:
457 # if not it'll pass the p_idx as an index below and crash
458 if p_idx < len(surface_splines[sp_idx].bezier_points):
459 p_co = surface_splines[sp_idx].bezier_points[p_idx].co
460 new_dist = (prev_p_co - p_co).length
462 # The new distance that could have the partial segment if
463 # it is still shorter than the target length
464 potential_segment_length = partial_segment_length + new_dist
466 # If the potential is still shorter, keep adding
467 if potential_segment_length < target_length:
468 partial_segment_length = potential_segment_length
470 p_idx += 1
471 prev_p_co = p_co
473 # If the potential is longer than the target, calculate the target
474 # (a point between the last two points), and assign
475 elif potential_segment_length > target_length:
476 remaining_dist = target_length - partial_segment_length
477 vec = p_co - prev_p_co
478 vec.normalize()
479 intermediate_co = prev_p_co + (vec * remaining_dist)
481 surface_splines_parsed[sp_idx].append(intermediate_co)
483 partial_segment_length += remaining_dist
484 prev_p_co = intermediate_co
486 finish_while = True
488 # If the potential is equal to the target, assign
489 elif potential_segment_length == target_length:
490 surface_splines_parsed[sp_idx].append(p_co)
491 prev_p_co = p_co
493 finish_while = True
495 if finish_while:
496 break
498 # last point of the spline
499 surface_splines_parsed[sp_idx].append(
500 surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
503 return surface_splines_parsed
505 # Counts the number of faces that belong to each edge
506 def edge_face_count(self, ob):
507 ed_keys_count_dict = {}
509 for face in ob.data.polygons:
510 for ed_keys in face.edge_keys:
511 if ed_keys not in ed_keys_count_dict:
512 ed_keys_count_dict[ed_keys] = 1
513 else:
514 ed_keys_count_dict[ed_keys] += 1
516 edge_face_count = []
517 for i in range(len(ob.data.edges)):
518 edge_face_count.append(0)
520 for i in range(len(ob.data.edges)):
521 ed = ob.data.edges[i]
523 v1 = ed.vertices[0]
524 v2 = ed.vertices[1]
526 if (v1, v2) in ed_keys_count_dict:
527 edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
528 elif (v2, v1) in ed_keys_count_dict:
529 edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
531 return edge_face_count
533 # Fills with faces all the selected vertices which form empty triangles or quads
534 def fill_with_faces(self, object):
535 all_selected_verts_count = self.main_object_selected_verts_count
537 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
539 # Calculate average length of selected edges
540 all_selected_verts = []
541 original_sel_edges_count = 0
542 for ed in object.data.edges:
543 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
544 coords = []
545 coords.append(object.data.vertices[ed.vertices[0]].co)
546 coords.append(object.data.vertices[ed.vertices[1]].co)
548 original_sel_edges_count += 1
550 if not ed.vertices[0] in all_selected_verts:
551 all_selected_verts.append(ed.vertices[0])
553 if not ed.vertices[1] in all_selected_verts:
554 all_selected_verts.append(ed.vertices[1])
556 tuple(all_selected_verts)
558 # Check if there is any edge selected. If not, interrupt the script
559 if original_sel_edges_count == 0 and all_selected_verts_count > 0:
560 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
584 if include_edge is True:
585 all_edges_around_sel_verts.append(ed_idx)
587 # Get all connected verts to each vert
588 if not ed.vertices[0] in verts_connected_to_every_vert:
589 verts_connected_to_every_vert[ed.vertices[0]] = []
591 if not ed.vertices[1] in verts_connected_to_every_vert:
592 verts_connected_to_every_vert[ed.vertices[1]] = []
594 verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
595 verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
597 # Get all verts connected to faces
598 all_verts_part_of_faces = []
599 all_edges_faces_count = []
600 all_edges_faces_count += self.edge_face_count(object)
602 # Get only the selected edges that have faces attached.
603 count_faces_of_edges_around_sel_verts = {}
604 selected_verts_with_faces = []
605 for ed_idx in all_edges_around_sel_verts:
606 count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
608 if all_edges_faces_count[ed_idx] > 0:
609 ed = object.data.edges[ed_idx]
611 if not ed.vertices[0] in selected_verts_with_faces:
612 selected_verts_with_faces.append(ed.vertices[0])
614 if not ed.vertices[1] in selected_verts_with_faces:
615 selected_verts_with_faces.append(ed.vertices[1])
617 all_verts_part_of_faces.append(ed.vertices[0])
618 all_verts_part_of_faces.append(ed.vertices[1])
620 tuple(selected_verts_with_faces)
622 # Discard unneeded verts from calculations
623 participating_verts = []
624 movable_verts = []
625 for v_idx in all_selected_verts:
626 vert_has_edges_with_one_face = False
628 # Check if the actual vert has at least one edge connected to only one face
629 for ed_idx in edges_connected_to_sel_verts[v_idx]:
630 if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
631 vert_has_edges_with_one_face = True
633 # If the vert has two or less edges connected and the vert is not part of any face.
634 # Or the vert is part of any face and at least one of
635 # the connected edges has only one face attached to it.
636 if (len(edges_connected_to_sel_verts[v_idx]) == 2 and
637 v_idx not in all_verts_part_of_faces) or \
638 len(edges_connected_to_sel_verts[v_idx]) == 1 or \
639 (v_idx in all_verts_part_of_faces and
640 vert_has_edges_with_one_face):
642 participating_verts.append(v_idx)
644 if v_idx not in all_verts_part_of_faces:
645 movable_verts.append(v_idx)
647 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
648 for mv_idx in movable_verts:
649 freeze_vert = False
650 mv_connected_verts = verts_connected_to_every_vert[mv_idx]
652 for actual_v_idx in all_selected_verts:
653 count_shared_neighbors = 0
654 checked_verts = []
656 for mv_conn_v_idx in mv_connected_verts:
657 if mv_idx != actual_v_idx:
658 if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and \
659 mv_conn_v_idx not in checked_verts:
660 count_shared_neighbors += 1
661 checked_verts.append(mv_conn_v_idx)
663 if actual_v_idx in mv_connected_verts:
664 freeze_vert = True
665 break
667 if count_shared_neighbors == 2:
668 freeze_vert = True
669 break
671 if freeze_vert:
672 break
674 if freeze_vert:
675 movable_verts.remove(mv_idx)
677 # Calculate merge distance for participating verts
678 shortest_edge_length = None
679 for ed in object.data.edges:
680 if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
681 v1 = object.data.vertices[ed.vertices[0]]
682 v2 = object.data.vertices[ed.vertices[1]]
684 length = (v1.co - v2.co).length
686 if shortest_edge_length is None:
687 shortest_edge_length = length
688 else:
689 if length < shortest_edge_length:
690 shortest_edge_length = length
692 if shortest_edge_length is not None:
693 edges_merge_distance = shortest_edge_length * 0.5
694 else:
695 edges_merge_distance = 0
697 # Get together the verts near enough. They will be merged later
698 remaining_verts = []
699 remaining_verts += participating_verts
700 for v1_idx in participating_verts:
701 if v1_idx in remaining_verts and v1_idx in movable_verts:
702 verts_to_merge = []
703 coords_verts_to_merge = {}
705 verts_to_merge.append(v1_idx)
707 v1_co = object.data.vertices[v1_idx].co
708 coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
710 for v2_idx in remaining_verts:
711 if v1_idx != v2_idx:
712 v2_co = object.data.vertices[v2_idx].co
714 dist = (v1_co - v2_co).length
716 if dist <= edges_merge_distance: # Add the verts which are near enough
717 verts_to_merge.append(v2_idx)
719 coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
721 for vm_idx in verts_to_merge:
722 remaining_verts.remove(vm_idx)
724 if len(verts_to_merge) > 1:
725 # Calculate middle point of the verts to merge.
726 sum_x_co = 0
727 sum_y_co = 0
728 sum_z_co = 0
729 movable_verts_to_merge_count = 0
730 for i in range(len(verts_to_merge)):
731 if verts_to_merge[i] in movable_verts:
732 v_co = object.data.vertices[verts_to_merge[i]].co
734 sum_x_co += v_co[0]
735 sum_y_co += v_co[1]
736 sum_z_co += v_co[2]
738 movable_verts_to_merge_count += 1
740 middle_point_co = [
741 sum_x_co / movable_verts_to_merge_count,
742 sum_y_co / movable_verts_to_merge_count,
743 sum_z_co / movable_verts_to_merge_count
746 # Check if any vert to be merged is not movable
747 shortest_dist = None
748 are_verts_not_movable = False
749 verts_not_movable = []
750 for v_merge_idx in verts_to_merge:
751 if v_merge_idx in participating_verts and v_merge_idx not in movable_verts:
752 are_verts_not_movable = True
753 verts_not_movable.append(v_merge_idx)
755 if are_verts_not_movable:
756 # Get the vert connected to faces, that is nearest to
757 # the middle point of the movable verts
758 shortest_dist = None
759 for vcf_idx in verts_not_movable:
760 dist = abs((object.data.vertices[vcf_idx].co -
761 Vector(middle_point_co)).length)
763 if shortest_dist is None:
764 shortest_dist = dist
765 nearest_vert_idx = vcf_idx
766 else:
767 if dist < shortest_dist:
768 shortest_dist = dist
769 nearest_vert_idx = vcf_idx
771 coords = object.data.vertices[nearest_vert_idx].co
772 target_point_co = [coords[0], coords[1], coords[2]]
773 else:
774 target_point_co = middle_point_co
776 # Move verts to merge to the middle position
777 for v_merge_idx in verts_to_merge:
778 if v_merge_idx in movable_verts: # Only move the verts that are not part of faces
779 object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
780 object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
781 object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
783 # Perform "Remove Doubles" to weld all the disconnected verts
784 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
785 bpy.ops.mesh.remove_doubles(threshold=0.0001)
787 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
789 # Get all the definitive selected edges, after weldding
790 selected_edges = []
791 edges_per_vert = {} # Number of faces of each selected edge
792 for ed in object.data.edges:
793 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
794 selected_edges.append(ed.index)
796 # Save all the edges that belong to each vertex.
797 if not ed.vertices[0] in edges_per_vert:
798 edges_per_vert[ed.vertices[0]] = []
800 if not ed.vertices[1] in edges_per_vert:
801 edges_per_vert[ed.vertices[1]] = []
803 edges_per_vert[ed.vertices[0]].append(ed.index)
804 edges_per_vert[ed.vertices[1]].append(ed.index)
806 # Check if all the edges connected to each vert have two faces attached to them.
807 # To discard them later and make calculations faster
808 a = []
809 a += self.edge_face_count(object)
810 tuple(a)
811 verts_surrounded_by_faces = {}
812 for v_idx in edges_per_vert:
813 edges = edges_per_vert[v_idx]
814 edges_with_two_faces_count = 0
816 for ed_idx in edges_per_vert[v_idx]:
817 if a[ed_idx] == 2:
818 edges_with_two_faces_count += 1
820 if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
821 verts_surrounded_by_faces[v_idx] = True
822 else:
823 verts_surrounded_by_faces[v_idx] = False
825 # Get all the selected vertices
826 selected_verts_idx = []
827 for v in object.data.vertices:
828 if v.select:
829 selected_verts_idx.append(v.index)
831 # Get all the faces of the object
832 all_object_faces_verts_idx = []
833 for face in object.data.polygons:
834 face_verts = []
835 face_verts.append(face.vertices[0])
836 face_verts.append(face.vertices[1])
837 face_verts.append(face.vertices[2])
839 if len(face.vertices) == 4:
840 face_verts.append(face.vertices[3])
842 all_object_faces_verts_idx.append(face_verts)
844 # Deselect all vertices
845 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
846 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
847 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
849 # Make a dictionary with the verts related to each vert
850 related_key_verts = {}
851 for ed_idx in selected_edges:
852 ed = object.data.edges[ed_idx]
854 if not verts_surrounded_by_faces[ed.vertices[0]]:
855 if not ed.vertices[0] in related_key_verts:
856 related_key_verts[ed.vertices[0]] = []
858 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
859 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
861 if not verts_surrounded_by_faces[ed.vertices[1]]:
862 if not ed.vertices[1] in related_key_verts:
863 related_key_verts[ed.vertices[1]] = []
865 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
866 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
868 # Get groups of verts forming each face
869 faces_verts_idx = []
870 for v1 in related_key_verts: # verts-1 ....
871 for v2 in related_key_verts: # verts-2
872 if v1 != v2:
873 related_verts_in_common = []
874 v2_in_rel_v1 = False
875 v1_in_rel_v2 = False
876 for rel_v1 in related_key_verts[v1]:
877 # Check if related verts of verts-1 are related verts of verts-2
878 if rel_v1 in related_key_verts[v2]:
879 related_verts_in_common.append(rel_v1)
881 if v2 in related_key_verts[v1]:
882 v2_in_rel_v1 = True
884 if v1 in related_key_verts[v2]:
885 v1_in_rel_v2 = True
887 repeated_face = False
888 # If two verts have two related verts in common, they form a quad
889 if len(related_verts_in_common) == 2:
890 # Check if the face is already saved
891 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
893 for f_verts in all_faces_to_check_idx:
894 repeated_verts = 0
896 if len(f_verts) == 4:
897 if v1 in f_verts:
898 repeated_verts += 1
899 if v2 in f_verts:
900 repeated_verts += 1
901 if related_verts_in_common[0] in f_verts:
902 repeated_verts += 1
903 if related_verts_in_common[1] in f_verts:
904 repeated_verts += 1
906 if repeated_verts == len(f_verts):
907 repeated_face = True
908 break
910 if not repeated_face:
911 faces_verts_idx.append(
912 [v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
915 # If Two verts have one related vert in common and
916 # they are related to each other, they form a triangle
917 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
918 # Check if the face is already saved.
919 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
921 for f_verts in all_faces_to_check_idx:
922 repeated_verts = 0
924 if len(f_verts) == 3:
925 if v1 in f_verts:
926 repeated_verts += 1
927 if v2 in f_verts:
928 repeated_verts += 1
929 if related_verts_in_common[0] in f_verts:
930 repeated_verts += 1
932 if repeated_verts == len(f_verts):
933 repeated_face = True
934 break
936 if not repeated_face:
937 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
939 # Keep only the faces that don't overlap by ignoring quads
940 # that overlap with two adjacent triangles
941 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
942 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
943 for i in range(len(faces_verts_idx)):
944 for t in range(len(all_faces_to_check_idx)):
945 if i != t:
946 verts_in_common = 0
948 if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
949 for v_idx in all_faces_to_check_idx[t]:
950 if v_idx in faces_verts_idx[i]:
951 verts_in_common += 1
952 # If it doesn't have all it's vertices repeated in the other face
953 if verts_in_common == 3:
954 if i not in faces_to_not_include_idx:
955 faces_to_not_include_idx.append(i)
957 # Build faces discarding the ones in faces_to_not_include
958 me = object.data
959 bm = bmesh.new()
960 bm.from_mesh(me)
962 num_faces_created = 0
963 for i in range(len(faces_verts_idx)):
964 if i not in faces_to_not_include_idx:
965 bm.faces.new([bm.verts[v] for v in faces_verts_idx[i]])
967 num_faces_created += 1
969 bm.to_mesh(me)
970 bm.free()
972 for v_idx in selected_verts_idx:
973 self.main_object.data.vertices[v_idx].select = True
975 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
976 bpy.ops.mesh.normals_make_consistent(inside=False)
977 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
979 return num_faces_created
981 # Crosshatch skinning
982 def crosshatch_surface_invoke(self, ob_original_splines):
983 self.is_crosshatch = False
984 self.crosshatch_merge_distance = 0
986 objects_to_delete = [] # duplicated strokes to be deleted.
988 # If the main object uses modifiers deactivate them temporarily until the surface is joined
989 # (without this the surface verts merging with the main object doesn't work well)
990 self.modifiers_prev_viewport_state = []
991 if len(self.main_object.modifiers) > 0:
992 for m_idx in range(len(self.main_object.modifiers)):
993 self.modifiers_prev_viewport_state.append(
994 self.main_object.modifiers[m_idx].show_viewport
996 self.main_object.modifiers[m_idx].show_viewport = False
998 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
999 ob_original_splines.select = True
1000 bpy.context.scene.objects.active = ob_original_splines
1002 if len(ob_original_splines.data.splines) >= 2:
1003 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1004 ob_splines = bpy.context.object
1005 ob_splines.name = "SURFSKIO_NE_STR"
1007 # Get estimative merge distance (sum up the distances from the first point to
1008 # all other points, then average them and then divide them)
1009 first_point_dist_sum = 0
1010 first_dist = 0
1011 second_dist = 0
1012 coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
1013 for i in range(len(ob_splines.data.splines)):
1014 sp = ob_splines.data.splines[i]
1016 if coords_first_pt != sp.bezier_points[0].co:
1017 first_dist = (coords_first_pt - sp.bezier_points[0].co).length
1019 if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
1020 second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
1022 first_point_dist_sum += first_dist + second_dist
1024 if i == 0:
1025 if first_dist != 0:
1026 shortest_dist = first_dist
1027 elif second_dist != 0:
1028 shortest_dist = second_dist
1030 if shortest_dist > first_dist and first_dist != 0:
1031 shortest_dist = first_dist
1033 if shortest_dist > second_dist and second_dist != 0:
1034 shortest_dist = second_dist
1036 self.crosshatch_merge_distance = shortest_dist / 20
1038 # Recalculation of merge distance
1040 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1042 ob_calc_merge_dist = bpy.context.object
1043 ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
1045 objects_to_delete.append(ob_calc_merge_dist)
1047 # Smooth out strokes a little to improve crosshatch detection
1048 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1049 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
1051 for i in range(4):
1052 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1054 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1055 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1057 # Convert curves into mesh
1058 ob_calc_merge_dist.data.resolution_u = 12
1059 bpy.ops.object.convert(target='MESH', keep_original=False)
1061 # Find "intersection-nodes"
1062 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1063 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1064 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1065 threshold=self.crosshatch_merge_distance)
1066 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1067 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1069 # Remove verts with less than three edges
1070 verts_edges_count = {}
1071 for ed in ob_calc_merge_dist.data.edges:
1072 v = ed.vertices
1074 if v[0] not in verts_edges_count:
1075 verts_edges_count[v[0]] = 0
1077 if v[1] not in verts_edges_count:
1078 verts_edges_count[v[1]] = 0
1080 verts_edges_count[v[0]] += 1
1081 verts_edges_count[v[1]] += 1
1083 nodes_verts_coords = []
1084 for v_idx in verts_edges_count:
1085 v = ob_calc_merge_dist.data.vertices[v_idx]
1087 if verts_edges_count[v_idx] < 3:
1088 v.select = True
1090 # Remove them
1091 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1092 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
1093 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1095 # Remove doubles to discard very near verts from calculations of distance
1096 bpy.ops.mesh.remove_doubles(
1097 'INVOKE_REGION_WIN',
1098 threshold=self.crosshatch_merge_distance * 4.0
1100 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1102 # Get all coords of the resulting nodes
1103 nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for
1104 v in ob_calc_merge_dist.data.vertices]
1106 # Check if the strokes are a crosshatch
1107 if len(nodes_verts_coords) >= 3:
1108 self.is_crosshatch = True
1110 shortest_dist = None
1111 for co_1 in nodes_verts_coords:
1112 for co_2 in nodes_verts_coords:
1113 if co_1 != co_2:
1114 dist = (Vector(co_1) - Vector(co_2)).length
1116 if shortest_dist is not None:
1117 if dist < shortest_dist:
1118 shortest_dist = dist
1119 else:
1120 shortest_dist = dist
1122 self.crosshatch_merge_distance = shortest_dist / 3
1124 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1125 ob_splines.select = True
1126 bpy.context.scene.objects.active = ob_splines
1128 # Deselect all points
1129 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1130 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1131 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1133 # Smooth splines in a localized way, to eliminate "saw-teeth"
1134 # like shapes when there are many points
1135 for sp in ob_splines.data.splines:
1136 angle_sum = 0
1138 angle_limit = 2 # Degrees
1139 for t in range(len(sp.bezier_points)):
1140 # Because on each iteration it checks the "next two points"
1141 # of the actual. This way it doesn't go out of range
1142 if t <= len(sp.bezier_points) - 3:
1143 p1 = sp.bezier_points[t]
1144 p2 = sp.bezier_points[t + 1]
1145 p3 = sp.bezier_points[t + 2]
1147 vec_1 = p1.co - p2.co
1148 vec_2 = p2.co - p3.co
1150 if p2.co != p1.co and p2.co != p3.co:
1151 angle = vec_1.angle(vec_2)
1152 angle_sum += degrees(angle)
1154 if angle_sum >= angle_limit: # If sum of angles is grater than the limit
1155 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1156 p1.select_control_point = True
1157 p1.select_left_handle = True
1158 p1.select_right_handle = True
1160 p2.select_control_point = True
1161 p2.select_left_handle = True
1162 p2.select_right_handle = True
1164 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1165 p3.select_control_point = True
1166 p3.select_left_handle = True
1167 p3.select_right_handle = True
1169 angle_sum = 0
1171 sp.bezier_points[0].select_control_point = False
1172 sp.bezier_points[0].select_left_handle = False
1173 sp.bezier_points[0].select_right_handle = False
1175 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
1176 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
1177 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
1179 # Smooth out strokes a little to improve crosshatch detection
1180 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1182 for i in range(15):
1183 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1185 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1186 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1188 # Simplify the splines
1189 for sp in ob_splines.data.splines:
1190 angle_sum = 0
1192 sp.bezier_points[0].select_control_point = True
1193 sp.bezier_points[0].select_left_handle = True
1194 sp.bezier_points[0].select_right_handle = True
1196 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
1197 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
1198 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
1200 angle_limit = 15 # Degrees
1201 for t in range(len(sp.bezier_points)):
1202 # Because on each iteration it checks the "next two points"
1203 # of the actual. This way it doesn't go out of range
1204 if t <= len(sp.bezier_points) - 3:
1205 p1 = sp.bezier_points[t]
1206 p2 = sp.bezier_points[t + 1]
1207 p3 = sp.bezier_points[t + 2]
1209 vec_1 = p1.co - p2.co
1210 vec_2 = p2.co - p3.co
1212 if p2.co != p1.co and p2.co != p3.co:
1213 angle = vec_1.angle(vec_2)
1214 angle_sum += degrees(angle)
1215 # If sum of angles is grater than the limit
1216 if angle_sum >= angle_limit:
1217 p1.select_control_point = True
1218 p1.select_left_handle = True
1219 p1.select_right_handle = True
1221 p2.select_control_point = True
1222 p2.select_left_handle = True
1223 p2.select_right_handle = True
1225 p3.select_control_point = True
1226 p3.select_left_handle = True
1227 p3.select_right_handle = True
1229 angle_sum = 0
1231 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1232 bpy.ops.curve.select_all(action='INVERT')
1234 bpy.ops.curve.delete(type='VERT')
1235 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1237 objects_to_delete.append(ob_splines)
1239 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1240 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1241 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1243 # Check if the strokes are a crosshatch
1244 if self.is_crosshatch:
1245 all_points_coords = []
1246 for i in range(len(ob_splines.data.splines)):
1247 all_points_coords.append([])
1249 all_points_coords[i] = [Vector((x, y, z)) for
1250 x, y, z in [bp.co for
1251 bp in ob_splines.data.splines[i].bezier_points]]
1253 all_intersections = []
1254 checked_splines = []
1255 for i in range(len(all_points_coords)):
1257 for t in range(len(all_points_coords[i]) - 1):
1258 bp1_co = all_points_coords[i][t]
1259 bp2_co = all_points_coords[i][t + 1]
1261 for i2 in range(len(all_points_coords)):
1262 if i != i2 and i2 not in checked_splines:
1263 for t2 in range(len(all_points_coords[i2]) - 1):
1264 bp3_co = all_points_coords[i2][t2]
1265 bp4_co = all_points_coords[i2][t2 + 1]
1267 intersec_coords = intersect_line_line(
1268 bp1_co, bp2_co, bp3_co, bp4_co
1270 if intersec_coords is not None:
1271 dist = (intersec_coords[0] - intersec_coords[1]).length
1273 if dist <= self.crosshatch_merge_distance * 1.5:
1274 temp_co, percent1 = intersect_point_line(
1275 intersec_coords[0], bp1_co, bp2_co
1277 if (percent1 >= -0.02 and percent1 <= 1.02):
1278 temp_co, percent2 = intersect_point_line(
1279 intersec_coords[1], bp3_co, bp4_co
1281 if (percent2 >= -0.02 and percent2 <= 1.02):
1282 # Format: spline index, first point index from
1283 # corresponding segment, percentage from first point of
1284 # actual segment, coords of intersection point
1285 all_intersections.append(
1286 (i, t, percent1,
1287 ob_splines.matrix_world * intersec_coords[0])
1289 all_intersections.append(
1290 (i2, t2, percent2,
1291 ob_splines.matrix_world * intersec_coords[1])
1294 checked_splines.append(i)
1295 # Sort list by spline, then by corresponding first point index of segment,
1296 # and then by percentage from first point of segment: elements 0 and 1 respectively
1297 all_intersections.sort(key=operator.itemgetter(0, 1, 2))
1299 self.crosshatch_strokes_coords = {}
1300 for i in range(len(all_intersections)):
1301 if not all_intersections[i][0] in self.crosshatch_strokes_coords:
1302 self.crosshatch_strokes_coords[all_intersections[i][0]] = []
1304 self.crosshatch_strokes_coords[all_intersections[i][0]].append(
1305 all_intersections[i][3]
1306 ) # Save intersection coords
1307 else:
1308 self.is_crosshatch = False
1310 # Delete all duplicates
1311 for o in objects_to_delete:
1312 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1313 o.select = True
1314 bpy.context.scene.objects.active = o
1315 bpy.ops.object.delete()
1317 # If the main object has modifiers, turn their "viewport view status" to
1318 # what it was before the forced deactivation above
1319 if len(self.main_object.modifiers) > 0:
1320 for m_idx in range(len(self.main_object.modifiers)):
1321 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1323 return
1325 # Part of the Crosshatch process that is repeated when the operator is tweaked
1326 def crosshatch_surface_execute(self):
1327 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1328 # (without this the surface verts merging with the main object doesn't work well)
1329 self.modifiers_prev_viewport_state = []
1330 if len(self.main_object.modifiers) > 0:
1331 for m_idx in range(len(self.main_object.modifiers)):
1332 self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
1334 self.main_object.modifiers[m_idx].show_viewport = False
1336 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1338 me_name = "SURFSKIO_STK_TMP"
1339 me = bpy.data.meshes.new(me_name)
1341 all_verts_coords = []
1342 all_edges = []
1343 for st_idx in self.crosshatch_strokes_coords:
1344 for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
1345 coords = self.crosshatch_strokes_coords[st_idx][co_idx]
1347 all_verts_coords.append(coords)
1349 if co_idx > 0:
1350 all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
1352 me.from_pydata(all_verts_coords, all_edges, [])
1354 me.update()
1356 ob = bpy.data.objects.new(me_name, me)
1357 ob.data = me
1358 bpy.context.scene.objects.link(ob)
1360 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1361 ob.select = True
1362 bpy.context.scene.objects.active = ob
1364 # Get together each vert and its nearest, to the middle position
1365 verts = ob.data.vertices
1366 checked_verts = []
1367 for i in range(len(verts)):
1368 shortest_dist = None
1370 if i not in checked_verts:
1371 for t in range(len(verts)):
1372 if i != t and t not in checked_verts:
1373 dist = (verts[i].co - verts[t].co).length
1375 if shortest_dist is not None:
1376 if dist < shortest_dist:
1377 shortest_dist = dist
1378 nearest_vert = t
1379 else:
1380 shortest_dist = dist
1381 nearest_vert = t
1383 middle_location = (verts[i].co + verts[nearest_vert].co) / 2
1385 verts[i].co = middle_location
1386 verts[nearest_vert].co = middle_location
1388 checked_verts.append(i)
1389 checked_verts.append(nearest_vert)
1391 # Calculate average length between all the generated edges
1392 ob = bpy.context.object
1393 lengths_sum = 0
1394 for ed in ob.data.edges:
1395 v1 = ob.data.vertices[ed.vertices[0]]
1396 v2 = ob.data.vertices[ed.vertices[1]]
1398 lengths_sum += (v1.co - v2.co).length
1400 edges_count = len(ob.data.edges)
1401 # possible division by zero here
1402 average_edge_length = lengths_sum / edges_count if edges_count != 0 else 0.0001
1404 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1405 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1406 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1407 threshold=average_edge_length / 15.0)
1408 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1410 final_points_ob = bpy.context.scene.objects.active
1412 # Make a dictionary with the verts related to each vert
1413 related_key_verts = {}
1414 for ed in final_points_ob.data.edges:
1415 if not ed.vertices[0] in related_key_verts:
1416 related_key_verts[ed.vertices[0]] = []
1418 if not ed.vertices[1] in related_key_verts:
1419 related_key_verts[ed.vertices[1]] = []
1421 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
1422 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
1424 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
1425 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
1427 # Get groups of verts forming each face
1428 faces_verts_idx = []
1429 for v1 in related_key_verts: # verts-1 ....
1430 for v2 in related_key_verts: # verts-2
1431 if v1 != v2:
1432 related_verts_in_common = []
1433 v2_in_rel_v1 = False
1434 v1_in_rel_v2 = False
1435 for rel_v1 in related_key_verts[v1]:
1436 # Check if related verts of verts-1 are related verts of verts-2
1437 if rel_v1 in related_key_verts[v2]:
1438 related_verts_in_common.append(rel_v1)
1440 if v2 in related_key_verts[v1]:
1441 v2_in_rel_v1 = True
1443 if v1 in related_key_verts[v2]:
1444 v1_in_rel_v2 = True
1446 repeated_face = False
1447 # If two verts have two related verts in common, they form a quad
1448 if len(related_verts_in_common) == 2:
1449 # Check if the face is already saved
1450 for f_verts in faces_verts_idx:
1451 repeated_verts = 0
1453 if len(f_verts) == 4:
1454 if v1 in f_verts:
1455 repeated_verts += 1
1456 if v2 in f_verts:
1457 repeated_verts += 1
1458 if related_verts_in_common[0] in f_verts:
1459 repeated_verts += 1
1460 if related_verts_in_common[1] in f_verts:
1461 repeated_verts += 1
1463 if repeated_verts == len(f_verts):
1464 repeated_face = True
1465 break
1467 if not repeated_face:
1468 faces_verts_idx.append([v1, related_verts_in_common[0],
1469 v2, related_verts_in_common[1]])
1471 # If Two verts have one related vert in common and they are
1472 # related to each other, they form a triangle
1473 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1474 # Check if the face is already saved.
1475 for f_verts in faces_verts_idx:
1476 repeated_verts = 0
1478 if len(f_verts) == 3:
1479 if v1 in f_verts:
1480 repeated_verts += 1
1481 if v2 in f_verts:
1482 repeated_verts += 1
1483 if related_verts_in_common[0] in f_verts:
1484 repeated_verts += 1
1486 if repeated_verts == len(f_verts):
1487 repeated_face = True
1488 break
1490 if not repeated_face:
1491 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1493 # Keep only the faces that don't overlap by ignoring
1494 # quads that overlap with two adjacent triangles
1495 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1496 for i in range(len(faces_verts_idx)):
1497 for t in range(len(faces_verts_idx)):
1498 if i != t:
1499 verts_in_common = 0
1501 if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
1502 for v_idx in faces_verts_idx[t]:
1503 if v_idx in faces_verts_idx[i]:
1504 verts_in_common += 1
1505 # If it doesn't have all it's vertices repeated in the other face
1506 if verts_in_common == 3:
1507 if i not in faces_to_not_include_idx:
1508 faces_to_not_include_idx.append(i)
1510 # Build surface
1511 all_surface_verts_co = []
1512 verts_idx_translation = {}
1513 for i in range(len(final_points_ob.data.vertices)):
1514 coords = final_points_ob.data.vertices[i].co
1515 all_surface_verts_co.append([coords[0], coords[1], coords[2]])
1517 # Verts of each face.
1518 all_surface_faces = []
1519 for i in range(len(faces_verts_idx)):
1520 if i not in faces_to_not_include_idx:
1521 face = []
1522 for v_idx in faces_verts_idx[i]:
1523 face.append(v_idx)
1525 all_surface_faces.append(face)
1527 # Build the mesh
1528 surf_me_name = "SURFSKIO_surface"
1529 me_surf = bpy.data.meshes.new(surf_me_name)
1531 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
1533 me_surf.update()
1535 ob_surface = bpy.data.objects.new(surf_me_name, me_surf)
1536 bpy.context.scene.objects.link(ob_surface)
1538 # Delete final points temporal object
1539 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1540 final_points_ob.select = True
1541 bpy.context.scene.objects.active = final_points_ob
1543 bpy.ops.object.delete()
1545 # Delete isolated verts if there are any
1546 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1547 ob_surface.select = True
1548 bpy.context.scene.objects.active = ob_surface
1550 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1551 bpy.ops.mesh.select_all(action='DESELECT')
1552 bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
1553 bpy.ops.mesh.delete()
1554 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1556 # Join crosshatch results with original mesh
1558 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1559 edges_length_sum = 0
1560 for ed in ob_surface.data.edges:
1561 edges_length_sum += (
1562 ob_surface.data.vertices[ed.vertices[0]].co -
1563 ob_surface.data.vertices[ed.vertices[1]].co
1564 ).length
1566 if len(ob_surface.data.edges) > 0:
1567 average_surface_edges_length = edges_length_sum / len(ob_surface.data.edges)
1568 else:
1569 average_surface_edges_length = 0.0001
1571 # Make dictionary with all the verts connected to each vert, on the new surface object.
1572 surface_connected_verts = {}
1573 for ed in ob_surface.data.edges:
1574 if not ed.vertices[0] in surface_connected_verts:
1575 surface_connected_verts[ed.vertices[0]] = []
1577 surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
1579 if ed.vertices[1] not in surface_connected_verts:
1580 surface_connected_verts[ed.vertices[1]] = []
1582 surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
1584 # Duplicate the new surface object, and use shrinkwrap to
1585 # calculate later the nearest verts to the main object
1586 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1587 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1588 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1590 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1592 final_ob_duplicate = bpy.context.scene.objects.active
1594 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1595 shrinkwrap_modifier = final_ob_duplicate.modifiers[-1]
1596 shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
1597 shrinkwrap_modifier.target = self.main_object
1599 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier=shrinkwrap_modifier.name)
1601 # Make list with verts of original mesh as index and coords as value
1602 main_object_verts_coords = []
1603 for v in self.main_object.data.vertices:
1604 coords = self.main_object.matrix_world * v.co
1606 # To avoid problems when taking "-0.00" as a different value as "0.00"
1607 for c in range(len(coords)):
1608 if "%.3f" % coords[c] == "-0.00":
1609 coords[c] = 0
1611 main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
1613 tuple(main_object_verts_coords)
1615 # Determine which verts will be merged, snap them to the nearest verts
1616 # on the original verts, and get them selected
1617 crosshatch_verts_to_merge = []
1618 if self.automatic_join:
1619 for i in range(len(ob_surface.data.vertices)):
1620 # Calculate the distance from each of the connected verts to the actual vert,
1621 # and compare it with the distance they would have if joined.
1622 # If they don't change much, that vert can be joined
1623 merge_actual_vert = True
1624 if len(surface_connected_verts[i]) < 4:
1625 for c_v_idx in surface_connected_verts[i]:
1626 points_original = []
1627 points_original.append(ob_surface.data.vertices[c_v_idx].co)
1628 points_original.append(ob_surface.data.vertices[i].co)
1630 points_target = []
1631 points_target.append(ob_surface.data.vertices[c_v_idx].co)
1632 points_target.append(final_ob_duplicate.data.vertices[i].co)
1634 vec_A = points_original[0] - points_original[1]
1635 vec_B = points_target[0] - points_target[1]
1637 dist_A = (points_original[0] - points_original[1]).length
1638 dist_B = (points_target[0] - points_target[1]).length
1640 if not (
1641 points_original[0] == points_original[1] or
1642 points_target[0] == points_target[1]
1643 ): # If any vector's length is zero
1645 angle = vec_A.angle(vec_B) / pi
1646 else:
1647 angle = 0
1649 # Set a range of acceptable variation in the connected edges
1650 if dist_B > dist_A * 1.7 * self.join_stretch_factor or \
1651 dist_B < dist_A / 2 / self.join_stretch_factor or \
1652 angle >= 0.15 * self.join_stretch_factor:
1654 merge_actual_vert = False
1655 break
1656 else:
1657 merge_actual_vert = False
1659 if merge_actual_vert:
1660 coords = final_ob_duplicate.data.vertices[i].co
1661 # To avoid problems when taking "-0.000" as a different value as "0.00"
1662 for c in range(len(coords)):
1663 if "%.3f" % coords[c] == "-0.00":
1664 coords[c] = 0
1666 comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
1668 if comparison_coords in main_object_verts_coords:
1669 # Get the index of the vert with those coords in the main object
1670 main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords)
1672 if self.main_object.data.vertices[main_object_related_vert_idx].select is True or \
1673 self.main_object_selected_verts_count == 0:
1675 ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
1676 ob_surface.data.vertices[i].select = True
1677 crosshatch_verts_to_merge.append(i)
1679 # Make sure the vert in the main object is selected,
1680 # in case it wasn't selected and the "join crosshatch" option is active
1681 self.main_object.data.vertices[main_object_related_vert_idx].select = True
1683 # Delete duplicated object
1684 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1685 final_ob_duplicate.select = True
1686 bpy.context.scene.objects.active = final_ob_duplicate
1687 bpy.ops.object.delete()
1689 # Join crosshatched surface and main object
1690 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1691 ob_surface.select = True
1692 self.main_object.select = True
1693 bpy.context.scene.objects.active = self.main_object
1695 bpy.ops.object.join('INVOKE_REGION_WIN')
1697 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1698 # Perform Remove doubles to merge verts
1699 if not (self.automatic_join is False and self.main_object_selected_verts_count == 0):
1700 bpy.ops.mesh.remove_doubles(threshold=0.0001)
1702 bpy.ops.mesh.select_all(action='DESELECT')
1704 # If the main object has modifiers, turn their "viewport view status"
1705 # to what it was before the forced deactivation above
1706 if len(self.main_object.modifiers) > 0:
1707 for m_idx in range(len(self.main_object.modifiers)):
1708 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1710 return {'FINISHED'}
1712 def rectangular_surface(self):
1713 # Selected edges
1714 all_selected_edges_idx = []
1715 all_selected_verts = []
1716 all_verts_idx = []
1717 for ed in self.main_object.data.edges:
1718 if ed.select:
1719 all_selected_edges_idx.append(ed.index)
1721 # Selected vertices
1722 if not ed.vertices[0] in all_selected_verts:
1723 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
1724 if not ed.vertices[1] in all_selected_verts:
1725 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
1727 # All verts (both from each edge) to determine later
1728 # which are at the tips (those not repeated twice)
1729 all_verts_idx.append(ed.vertices[0])
1730 all_verts_idx.append(ed.vertices[1])
1732 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1733 all_chains_tips_idx = []
1734 for v_idx in all_verts_idx:
1735 if all_verts_idx.count(v_idx) < 2:
1736 all_chains_tips_idx.append(v_idx)
1738 edges_connected_to_tips = []
1739 for ed in self.main_object.data.edges:
1740 if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and \
1741 not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
1743 edges_connected_to_tips.append(ed)
1745 # Check closed selections
1746 # List with groups of three verts, where the first element of the pair is
1747 # the unselected vert of a closed selection and the other two elements are the
1748 # selected neighbor verts (it will be useful to determine which selection chain
1749 # the unselected vert belongs to, and determine the "middle-vertex")
1750 single_unselected_verts_and_neighbors = []
1752 # To identify a "closed" selection (a selection that is a closed chain except
1753 # for one vertex) find the vertex in common that have the edges connected to tips.
1754 # If there is a vertex in common, that one is the unselected vert that closes
1755 # the selection or is a "middle-vertex"
1756 single_unselected_verts = []
1757 for ed in edges_connected_to_tips:
1758 for ed_b in edges_connected_to_tips:
1759 if ed != ed_b:
1760 if ed.vertices[0] == ed_b.vertices[0] and \
1761 not self.main_object.data.vertices[ed.vertices[0]].select and \
1762 ed.vertices[0] not in single_unselected_verts:
1764 # The second element is one of the tips of the selected
1765 # vertices of the closed selection
1766 single_unselected_verts_and_neighbors.append(
1767 [ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]
1769 single_unselected_verts.append(ed.vertices[0])
1770 break
1771 elif ed.vertices[0] == ed_b.vertices[1] and \
1772 not self.main_object.data.vertices[ed.vertices[0]].select and \
1773 ed.vertices[0] not in single_unselected_verts:
1775 single_unselected_verts_and_neighbors.append(
1776 [ed.vertices[0], ed.vertices[1], ed_b.vertices[0]]
1778 single_unselected_verts.append(ed.vertices[0])
1779 break
1780 elif ed.vertices[1] == ed_b.vertices[0] and \
1781 not self.main_object.data.vertices[ed.vertices[1]].select and \
1782 ed.vertices[1] not in single_unselected_verts:
1784 single_unselected_verts_and_neighbors.append(
1785 [ed.vertices[1], ed.vertices[0], ed_b.vertices[1]]
1787 single_unselected_verts.append(ed.vertices[1])
1788 break
1789 elif ed.vertices[1] == ed_b.vertices[1] and \
1790 not self.main_object.data.vertices[ed.vertices[1]].select and \
1791 ed.vertices[1] not in single_unselected_verts:
1793 single_unselected_verts_and_neighbors.append(
1794 [ed.vertices[1], ed.vertices[0], ed_b.vertices[0]]
1796 single_unselected_verts.append(ed.vertices[1])
1797 break
1799 middle_vertex_idx = None
1800 tips_to_discard_idx = []
1802 # Check if there is a "middle-vertex", and get its index
1803 for i in range(0, len(single_unselected_verts_and_neighbors)):
1804 actual_chain_verts = self.get_ordered_verts(
1805 self.main_object, all_selected_edges_idx,
1806 all_verts_idx, single_unselected_verts_and_neighbors[i][1],
1807 None, None
1810 if single_unselected_verts_and_neighbors[i][2] != \
1811 actual_chain_verts[len(actual_chain_verts) - 1].index:
1813 middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
1814 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
1815 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
1817 # List with pairs of verts that belong to the tips of each selection chain (row)
1818 verts_tips_same_chain_idx = []
1819 if len(all_chains_tips_idx) >= 2:
1820 checked_v = []
1821 for i in range(0, len(all_chains_tips_idx)):
1822 if all_chains_tips_idx[i] not in checked_v:
1823 v_chain = self.get_ordered_verts(
1824 self.main_object, all_selected_edges_idx,
1825 all_verts_idx, all_chains_tips_idx[i],
1826 middle_vertex_idx, None
1829 verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
1831 checked_v.append(v_chain[0].index)
1832 checked_v.append(v_chain[len(v_chain) - 1].index)
1834 # Selection tips (vertices).
1835 verts_tips_parsed_idx = []
1836 if len(all_chains_tips_idx) >= 2:
1837 for spec_v_idx in all_chains_tips_idx:
1838 if (spec_v_idx not in tips_to_discard_idx):
1839 verts_tips_parsed_idx.append(spec_v_idx)
1841 # Identify the type of selection made by the user
1842 if middle_vertex_idx is not None:
1843 # If there are 4 tips (two selection chains), and
1844 # there is only one single unselected vert (the middle vert)
1845 if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1:
1846 selection_type = "TWO_CONNECTED"
1847 else:
1848 # The type of the selection was not identified, the script stops.
1849 self.report({'WARNING'}, "The selection isn't valid.")
1850 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1851 self.cleanup_on_interruption()
1852 self.stopping_errors = True
1854 return{'CANCELLED'}
1855 else:
1856 if len(all_chains_tips_idx) == 2: # If there are 2 tips
1857 selection_type = "SINGLE"
1858 elif len(all_chains_tips_idx) == 4: # If there are 4 tips
1859 selection_type = "TWO_NOT_CONNECTED"
1860 elif len(all_chains_tips_idx) == 0:
1861 if len(self.main_splines.data.splines) > 1:
1862 selection_type = "NO_SELECTION"
1863 else:
1864 # If the selection was not identified and there is only one stroke,
1865 # there's no possibility to build a surface, so the script is interrupted
1866 self.report({'WARNING'}, "The selection isn't valid.")
1867 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1868 self.cleanup_on_interruption()
1869 self.stopping_errors = True
1871 return{'CANCELLED'}
1872 else:
1873 # The type of the selection was not identified, the script stops
1874 self.report({'WARNING'}, "The selection isn't valid.")
1876 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1877 self.cleanup_on_interruption()
1879 self.stopping_errors = True
1881 return{'CANCELLED'}
1883 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1884 if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
1885 self.report({'WARNING'},
1886 "At least two strokes are needed when there are two not connected selections")
1887 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1888 self.cleanup_on_interruption()
1889 self.stopping_errors = True
1891 return{'CANCELLED'}
1893 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1895 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1896 self.main_splines.select = True
1897 bpy.context.scene.objects.active = self.main_splines
1899 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1900 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1901 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1902 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1903 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1904 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1905 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1906 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1907 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1909 self.selection_U_exists = False
1910 self.selection_U2_exists = False
1911 self.selection_V_exists = False
1912 self.selection_V2_exists = False
1914 self.selection_U_is_closed = False
1915 self.selection_U2_is_closed = False
1916 self.selection_V_is_closed = False
1917 self.selection_V2_is_closed = False
1919 # Define what vertices are at the tips of each selection and are not the middle-vertex
1920 if selection_type == "TWO_CONNECTED":
1921 self.selection_U_exists = True
1922 self.selection_V_exists = True
1924 closing_vert_U_idx = None
1925 closing_vert_V_idx = None
1926 closing_vert_U2_idx = None
1927 closing_vert_V2_idx = None
1929 # Determine which selection is Selection-U and which is Selection-V
1930 points_A = []
1931 points_B = []
1932 points_first_stroke_tips = []
1934 points_A.append(
1935 self.main_object.matrix_world * self.main_object.data.vertices[verts_tips_parsed_idx[0]].co
1937 points_A.append(
1938 self.main_object.matrix_world * self.main_object.data.vertices[middle_vertex_idx].co
1940 points_B.append(
1941 self.main_object.matrix_world * self.main_object.data.vertices[verts_tips_parsed_idx[1]].co
1943 points_B.append(
1944 self.main_object.matrix_world * self.main_object.data.vertices[middle_vertex_idx].co
1946 points_first_stroke_tips.append(
1947 self.main_splines.data.splines[0].bezier_points[0].co
1949 points_first_stroke_tips.append(
1950 self.main_splines.data.splines[0].bezier_points[
1951 len(self.main_splines.data.splines[0].bezier_points) - 1
1952 ].co
1955 angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
1956 angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
1958 if angle_A < angle_B:
1959 first_vert_U_idx = verts_tips_parsed_idx[0]
1960 first_vert_V_idx = verts_tips_parsed_idx[1]
1961 else:
1962 first_vert_U_idx = verts_tips_parsed_idx[1]
1963 first_vert_V_idx = verts_tips_parsed_idx[0]
1965 elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
1966 first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
1967 last_sketched_point_first_stroke_co = \
1968 self.main_splines.data.splines[0].bezier_points[
1969 len(self.main_splines.data.splines[0].bezier_points) - 1
1970 ].co
1971 first_sketched_point_last_stroke_co = \
1972 self.main_splines.data.splines[
1973 len(self.main_splines.data.splines) - 1
1974 ].bezier_points[0].co
1975 if len(self.main_splines.data.splines) > 1:
1976 first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
1977 last_sketched_point_second_stroke_co = \
1978 self.main_splines.data.splines[1].bezier_points[
1979 len(self.main_splines.data.splines[1].bezier_points) - 1
1980 ].co
1982 single_unselected_neighbors = [] # Only the neighbors of the single unselected verts
1983 for verts_neig_idx in single_unselected_verts_and_neighbors:
1984 single_unselected_neighbors.append(verts_neig_idx[1])
1985 single_unselected_neighbors.append(verts_neig_idx[2])
1987 all_chains_tips_and_middle_vert = []
1988 for v_idx in all_chains_tips_idx:
1989 if v_idx not in single_unselected_neighbors:
1990 all_chains_tips_and_middle_vert.append(v_idx)
1992 all_chains_tips_and_middle_vert += single_unselected_verts
1994 all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
1996 # The tip of the selected vertices nearest to the first point of the first sketched stroke
1997 nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = \
1998 self.shortest_distance(
1999 self.main_object,
2000 first_sketched_point_first_stroke_co,
2001 all_chains_tips_and_middle_vert
2003 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2004 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2005 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2007 nearest_tip_to_first_st_first_pt_opposite_idx = \
2008 self.opposite_tip(
2009 nearest_tip_to_first_st_first_pt_idx,
2010 verts_tips_same_chain_idx
2012 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2013 nearest_tip_to_first_st_last_pt_idx, temp_dist = \
2014 self.shortest_distance(
2015 self.main_object,
2016 last_sketched_point_first_stroke_co,
2017 all_chains_tips_and_middle_vert
2019 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2020 nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = \
2021 self.shortest_distance(
2022 self.main_object,
2023 first_sketched_point_last_stroke_co,
2024 all_chains_tips_and_middle_vert
2026 if len(self.main_splines.data.splines) > 1:
2027 # The selected vertex nearest to the first point of the second sketched stroke
2028 # (This will be useful to determine the direction of the closed
2029 # selection V when extruding along strokes)
2030 nearest_vert_to_second_st_first_pt_idx, temp_dist = \
2031 self.shortest_distance(
2032 self.main_object,
2033 first_sketched_point_second_stroke_co,
2034 all_verts_idx
2036 # The selected vertex nearest to the first point of the second sketched stroke
2037 # (This will be useful to determine the direction of the closed
2038 # selection V2 when extruding along strokes)
2039 nearest_vert_to_second_st_last_pt_idx, temp_dist = \
2040 self.shortest_distance(
2041 self.main_object,
2042 last_sketched_point_second_stroke_co,
2043 all_verts_idx
2045 # Determine if the single selection will be treated as U or as V
2046 edges_sum = 0
2047 for i in all_selected_edges_idx:
2048 edges_sum += (
2049 (self.main_object.matrix_world *
2050 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) -
2051 (self.main_object.matrix_world *
2052 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)
2053 ).length
2055 average_edge_length = edges_sum / len(all_selected_edges_idx)
2057 # Get shortest distance from the first point of the last stroke to any participating vertex
2058 temp_idx, shortest_distance_to_last_stroke = \
2059 self.shortest_distance(
2060 self.main_object,
2061 first_sketched_point_last_stroke_co,
2062 all_participating_verts
2064 # If the beginning of the first stroke is near enough, and its orientation
2065 # difference with the first edge of the nearest selection chain is not too high,
2066 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2067 if shortest_distance_to_first_stroke < average_edge_length / 4 and \
2068 shortest_distance_to_last_stroke < average_edge_length and \
2069 len(self.main_splines.data.splines) > 1:
2071 self.selection_U_exists = False
2072 self.selection_V_exists = True
2073 # If the first selection is not closed
2074 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2075 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2076 self.selection_V_is_closed = False
2077 first_neighbor_V_idx = None
2078 closing_vert_U_idx = None
2079 closing_vert_U2_idx = None
2080 closing_vert_V_idx = None
2081 closing_vert_V2_idx = None
2083 first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2085 if selection_type == "TWO_NOT_CONNECTED":
2086 self.selection_V2_exists = True
2088 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2089 else:
2090 self.selection_V_is_closed = True
2091 closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2093 # Get the neighbors of the first (unselected) vert of the closed selection U.
2094 vert_neighbors = []
2095 for verts in single_unselected_verts_and_neighbors:
2096 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2097 vert_neighbors.append(verts[1])
2098 vert_neighbors.append(verts[2])
2099 break
2101 verts_V = self.get_ordered_verts(
2102 self.main_object, all_selected_edges_idx,
2103 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2106 for i in range(0, len(verts_V)):
2107 if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
2108 # If the vertex nearest to the first point of the second stroke
2109 # is in the first half of the selected verts
2110 if i >= len(verts_V) / 2:
2111 first_vert_V_idx = vert_neighbors[1]
2112 break
2113 else:
2114 first_vert_V_idx = vert_neighbors[0]
2115 break
2117 if selection_type == "TWO_NOT_CONNECTED":
2118 self.selection_V2_exists = True
2119 # If the second selection is not closed
2120 if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or \
2121 nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx:
2123 self.selection_V2_is_closed = False
2124 first_neighbor_V2_idx = None
2125 closing_vert_V2_idx = None
2126 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2128 else:
2129 self.selection_V2_is_closed = True
2130 closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2132 # Get the neighbors of the first (unselected) vert of the closed selection U
2133 vert_neighbors = []
2134 for verts in single_unselected_verts_and_neighbors:
2135 if verts[0] == nearest_tip_to_first_st_last_pt_idx:
2136 vert_neighbors.append(verts[1])
2137 vert_neighbors.append(verts[2])
2138 break
2140 verts_V2 = self.get_ordered_verts(
2141 self.main_object, all_selected_edges_idx,
2142 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2145 for i in range(0, len(verts_V2)):
2146 if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
2147 # If the vertex nearest to the first point of the second stroke
2148 # is in the first half of the selected verts
2149 if i >= len(verts_V2) / 2:
2150 first_vert_V2_idx = vert_neighbors[1]
2151 break
2152 else:
2153 first_vert_V2_idx = vert_neighbors[0]
2154 break
2155 else:
2156 self.selection_V2_exists = False
2158 else:
2159 self.selection_U_exists = True
2160 self.selection_V_exists = False
2161 # If the first selection is not closed
2162 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2163 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2164 self.selection_U_is_closed = False
2165 first_neighbor_U_idx = None
2166 closing_vert_U_idx = None
2168 points_tips = []
2169 points_tips.append(
2170 self.main_object.matrix_world *
2171 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2173 points_tips.append(
2174 self.main_object.matrix_world *
2175 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co
2177 points_first_stroke_tips = []
2178 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2179 points_first_stroke_tips.append(
2180 self.main_splines.data.splines[0].bezier_points[
2181 len(self.main_splines.data.splines[0].bezier_points) - 1
2182 ].co
2184 vec_A = points_tips[0] - points_tips[1]
2185 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2187 # Compare the direction of the selection and the first
2188 # grease pencil stroke to determine which is the "first" vertex of the selection
2189 if vec_A.dot(vec_B) < 0:
2190 first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
2191 else:
2192 first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2194 else:
2195 self.selection_U_is_closed = True
2196 closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2198 # Get the neighbors of the first (unselected) vert of the closed selection U
2199 vert_neighbors = []
2200 for verts in single_unselected_verts_and_neighbors:
2201 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2202 vert_neighbors.append(verts[1])
2203 vert_neighbors.append(verts[2])
2204 break
2206 points_first_and_neighbor = []
2207 points_first_and_neighbor.append(
2208 self.main_object.matrix_world *
2209 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2211 points_first_and_neighbor.append(
2212 self.main_object.matrix_world *
2213 self.main_object.data.vertices[vert_neighbors[0]].co
2215 points_first_stroke_tips = []
2216 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2217 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
2219 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2220 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2222 # Compare the direction of the selection and the first grease pencil stroke to
2223 # determine which is the vertex neighbor to the first vertex (unselected) of
2224 # the closed selection. This will determine the direction of the closed selection
2225 if vec_A.dot(vec_B) < 0:
2226 first_vert_U_idx = vert_neighbors[1]
2227 else:
2228 first_vert_U_idx = vert_neighbors[0]
2230 if selection_type == "TWO_NOT_CONNECTED":
2231 self.selection_U2_exists = True
2232 # If the second selection is not closed
2233 if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or \
2234 nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx:
2236 self.selection_U2_is_closed = False
2237 first_neighbor_U2_idx = None
2238 closing_vert_U2_idx = None
2239 first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2240 else:
2241 self.selection_U2_is_closed = True
2242 closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2244 # Get the neighbors of the first (unselected) vert of the closed selection U
2245 vert_neighbors = []
2246 for verts in single_unselected_verts_and_neighbors:
2247 if verts[0] == nearest_tip_to_last_st_first_pt_idx:
2248 vert_neighbors.append(verts[1])
2249 vert_neighbors.append(verts[2])
2250 break
2252 points_first_and_neighbor = []
2253 points_first_and_neighbor.append(
2254 self.main_object.matrix_world *
2255 self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co
2257 points_first_and_neighbor.append(
2258 self.main_object.matrix_world *
2259 self.main_object.data.vertices[vert_neighbors[0]].co
2261 points_last_stroke_tips = []
2262 points_last_stroke_tips.append(
2263 self.main_splines.data.splines[
2264 len(self.main_splines.data.splines) - 1
2265 ].bezier_points[0].co
2267 points_last_stroke_tips.append(
2268 self.main_splines.data.splines[
2269 len(self.main_splines.data.splines) - 1
2270 ].bezier_points[1].co
2272 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2273 vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
2275 # Compare the direction of the selection and the last grease pencil stroke to
2276 # determine which is the vertex neighbor to the first vertex (unselected) of
2277 # the closed selection. This will determine the direction of the closed selection
2278 if vec_A.dot(vec_B) < 0:
2279 first_vert_U2_idx = vert_neighbors[1]
2280 else:
2281 first_vert_U2_idx = vert_neighbors[0]
2282 else:
2283 self.selection_U2_exists = False
2285 elif selection_type == "NO_SELECTION":
2286 self.selection_U_exists = False
2287 self.selection_V_exists = False
2289 # Get an ordered list of the vertices of Selection-U
2290 verts_ordered_U = []
2291 if self.selection_U_exists:
2292 verts_ordered_U = self.get_ordered_verts(
2293 self.main_object, all_selected_edges_idx,
2294 all_verts_idx, first_vert_U_idx,
2295 middle_vertex_idx, closing_vert_U_idx
2297 verts_ordered_U_indices = [x.index for x in verts_ordered_U]
2299 # Get an ordered list of the vertices of Selection-U2
2300 verts_ordered_U2 = []
2301 if self.selection_U2_exists:
2302 verts_ordered_U2 = self.get_ordered_verts(
2303 self.main_object, all_selected_edges_idx,
2304 all_verts_idx, first_vert_U2_idx,
2305 middle_vertex_idx, closing_vert_U2_idx
2307 verts_ordered_U2_indices = [x.index for x in verts_ordered_U2]
2309 # Get an ordered list of the vertices of Selection-V
2310 verts_ordered_V = []
2311 if self.selection_V_exists:
2312 verts_ordered_V = self.get_ordered_verts(
2313 self.main_object, all_selected_edges_idx,
2314 all_verts_idx, first_vert_V_idx,
2315 middle_vertex_idx, closing_vert_V_idx
2317 verts_ordered_V_indices = [x.index for x in verts_ordered_V]
2319 # Get an ordered list of the vertices of Selection-V2
2320 verts_ordered_V2 = []
2321 if self.selection_V2_exists:
2322 verts_ordered_V2 = self.get_ordered_verts(
2323 self.main_object, all_selected_edges_idx,
2324 all_verts_idx, first_vert_V2_idx,
2325 middle_vertex_idx, closing_vert_V2_idx
2327 verts_ordered_V2_indices = [x.index for x in verts_ordered_V2]
2329 # Check if when there are two-not-connected selections both have the same
2330 # number of verts. If not terminate the script
2331 if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or
2332 (self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
2333 # Display a warning
2334 self.report({'WARNING'}, "Both selections must have the same number of edges")
2336 self.cleanup_on_interruption()
2337 self.stopping_errors = True
2339 return{'CANCELLED'}
2341 # Calculate edges U proportions
2342 # Sum selected edges U lengths
2343 edges_lengths_U = []
2344 edges_lengths_sum_U = 0
2346 if self.selection_U_exists:
2347 edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(
2348 self.main_object,
2349 verts_ordered_U
2351 if self.selection_U2_exists:
2352 edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(
2353 self.main_object,
2354 verts_ordered_U2
2356 # Sum selected edges V lengths
2357 edges_lengths_V = []
2358 edges_lengths_sum_V = 0
2360 if self.selection_V_exists:
2361 edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(
2362 self.main_object,
2363 verts_ordered_V
2365 if self.selection_V2_exists:
2366 edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(
2367 self.main_object,
2368 verts_ordered_V2
2371 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2372 bpy.ops.curve.subdivide('INVOKE_REGION_WIN',
2373 number_cuts=bpy.context.scene.bsurfaces.SURFSK_precision)
2374 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2376 # Proportions U
2377 edges_proportions_U = []
2378 edges_proportions_U = self.get_edges_proportions(
2379 edges_lengths_U, edges_lengths_sum_U,
2380 self.selection_U_exists, self.edges_U
2382 verts_count_U = len(edges_proportions_U) + 1
2384 if self.selection_U2_exists:
2385 edges_proportions_U2 = []
2386 edges_proportions_U2 = self.get_edges_proportions(
2387 edges_lengths_U2, edges_lengths_sum_U2,
2388 self.selection_U2_exists, self.edges_V
2390 verts_count_U2 = len(edges_proportions_U2) + 1
2392 # Proportions V
2393 edges_proportions_V = []
2394 edges_proportions_V = self.get_edges_proportions(
2395 edges_lengths_V, edges_lengths_sum_V,
2396 self.selection_V_exists, self.edges_V
2398 verts_count_V = len(edges_proportions_V) + 1
2400 if self.selection_V2_exists:
2401 edges_proportions_V2 = []
2402 edges_proportions_V2 = self.get_edges_proportions(
2403 edges_lengths_V2, edges_lengths_sum_V2,
2404 self.selection_V2_exists, self.edges_V
2406 verts_count_V2 = len(edges_proportions_V2) + 1
2408 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2409 # the actual sketched curves with a "closing segment"
2410 if self.cyclic_follow and not self.selection_V_exists and not \
2411 ((self.selection_U_exists and not self.selection_U_is_closed) or
2412 (self.selection_U2_exists and not self.selection_U2_is_closed)):
2414 simplified_spline_coords = []
2415 simplified_curve = []
2416 ob_simplified_curve = []
2417 splines_first_v_co = []
2418 for i in range(len(self.main_splines.data.splines)):
2419 # Create a curve object for the actual spline "cyclic extension"
2420 simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
2421 ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
2422 bpy.context.scene.objects.link(ob_simplified_curve[i])
2424 simplified_curve[i].dimensions = "3D"
2426 spline_coords = []
2427 for bp in self.main_splines.data.splines[i].bezier_points:
2428 spline_coords.append(bp.co)
2430 # Simplification
2431 simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
2433 # Get the coordinates of the first vert of the actual spline
2434 splines_first_v_co.append(simplified_spline_coords[i][0])
2436 # Generate the spline
2437 spline = simplified_curve[i].splines.new('BEZIER')
2438 # less one because one point is added when the spline is created
2439 spline.bezier_points.add(len(simplified_spline_coords[i]) - 1)
2440 for p in range(0, len(simplified_spline_coords[i])):
2441 spline.bezier_points[p].co = simplified_spline_coords[i][p]
2443 spline.use_cyclic_u = True
2445 spline_bp_count = len(spline.bezier_points)
2447 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2448 ob_simplified_curve[i].select = True
2449 bpy.context.scene.objects.active = ob_simplified_curve[i]
2451 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2452 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
2453 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2454 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
2455 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2457 # Select the "closing segment", and subdivide it
2458 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
2459 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
2460 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
2462 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
2463 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
2464 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
2466 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2467 segments = sqrt(
2468 (ob_simplified_curve[i].data.splines[0].bezier_points[0].co -
2469 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length /
2470 self.average_gp_segment_length
2472 for t in range(2):
2473 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=segments)
2475 # Delete the other vertices and make it non-cyclic to
2476 # keep only the needed verts of the "closing segment"
2477 bpy.ops.curve.select_all(action='INVERT')
2478 bpy.ops.curve.delete(type='VERT')
2479 ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
2480 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2482 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2483 first_new_index = len(self.main_splines.data.splines[i].bezier_points)
2484 self.main_splines.data.splines[i].bezier_points.add(
2485 len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1
2487 for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
2488 self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = \
2489 ob_simplified_curve[i].data.splines[0].bezier_points[t].co
2491 # Delete the temporal curve
2492 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2493 ob_simplified_curve[i].select = True
2494 bpy.context.scene.objects.active = ob_simplified_curve[i]
2496 bpy.ops.object.delete()
2498 # Get the coords of the points distributed along the sketched strokes,
2499 # with proportions-U of the first selection
2500 pts_on_strokes_with_proportions_U = self.distribute_pts(
2501 self.main_splines.data.splines,
2502 edges_proportions_U
2504 sketched_splines_parsed = []
2506 if self.selection_U2_exists:
2507 # Initialize the multidimensional list with the proportions of all the segments
2508 proportions_loops_crossing_strokes = []
2509 for i in range(len(pts_on_strokes_with_proportions_U)):
2510 proportions_loops_crossing_strokes.append([])
2512 for t in range(len(pts_on_strokes_with_proportions_U[0])):
2513 proportions_loops_crossing_strokes[i].append(None)
2515 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2516 for lp in range(len(pts_on_strokes_with_proportions_U[0])):
2517 loop_segments_lengths = []
2519 for st in range(len(pts_on_strokes_with_proportions_U)):
2520 # When on the first stroke, add the segment from the selection to the dirst stroke
2521 if st == 0:
2522 loop_segments_lengths.append(
2523 ((self.main_object.matrix_world * verts_ordered_U[lp].co) -
2524 pts_on_strokes_with_proportions_U[0][lp]).length
2526 # For all strokes except for the last, calculate the distance
2527 # from the actual stroke to the next
2528 if st != len(pts_on_strokes_with_proportions_U) - 1:
2529 loop_segments_lengths.append(
2530 (pts_on_strokes_with_proportions_U[st][lp] -
2531 pts_on_strokes_with_proportions_U[st + 1][lp]).length
2533 # When on the last stroke, add the segments
2534 # from the last stroke to the second selection
2535 if st == len(pts_on_strokes_with_proportions_U) - 1:
2536 loop_segments_lengths.append(
2537 (pts_on_strokes_with_proportions_U[st][lp] -
2538 (self.main_object.matrix_world * verts_ordered_U2[lp].co)).length
2540 # Calculate full loop length
2541 loop_seg_lengths_sum = 0
2542 for i in range(len(loop_segments_lengths)):
2543 loop_seg_lengths_sum += loop_segments_lengths[i]
2545 # Fill the multidimensional list with the proportions of all the segments
2546 for st in range(len(pts_on_strokes_with_proportions_U)):
2547 proportions_loops_crossing_strokes[st][lp] = \
2548 loop_segments_lengths[st] / loop_seg_lengths_sum
2550 # Calculate proportions for each stroke
2551 for st in range(len(pts_on_strokes_with_proportions_U)):
2552 actual_stroke_spline = []
2553 # Needs to be a list for the "distribute_pts" method
2554 actual_stroke_spline.append(self.main_splines.data.splines[st])
2556 # Calculate the proportions for the actual stroke.
2557 actual_edges_proportions_U = []
2558 for i in range(len(edges_proportions_U)):
2559 proportions_sum = 0
2561 # Sum the proportions of this loop up to the actual.
2562 for t in range(0, st + 1):
2563 proportions_sum += proportions_loops_crossing_strokes[t][i]
2564 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2565 # and the proportions refer to edges, so we start at the element 1
2566 # of proportions_loops_crossing_strokes instead of element 0
2567 actual_edges_proportions_U.append(
2568 edges_proportions_U[i] -
2569 ((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)
2571 points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
2572 sketched_splines_parsed.append(points_actual_spline[0])
2573 else:
2574 sketched_splines_parsed = pts_on_strokes_with_proportions_U
2576 # If the selection type is "TWO_NOT_CONNECTED" replace the
2577 # points of the last spline with the points in the "target" selection
2578 if selection_type == "TWO_NOT_CONNECTED":
2579 if self.selection_U2_exists:
2580 for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
2581 sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = \
2582 self.main_object.matrix_world * verts_ordered_U2[i].co
2584 # Create temporary curves along the "control-points" found
2585 # on the sketched curves and the mesh selection
2586 mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
2587 me = bpy.data.meshes.new(mesh_ctrl_pts_name)
2588 ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
2589 ob_ctrl_pts.data = me
2590 bpy.context.scene.objects.link(ob_ctrl_pts)
2592 cyclic_loops_U = []
2593 first_verts = []
2594 second_verts = []
2595 last_verts = []
2597 for i in range(0, verts_count_U):
2598 vert_num_in_spline = 1
2600 if self.selection_U_exists:
2601 ob_ctrl_pts.data.vertices.add(1)
2602 last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2603 last_v.co = self.main_object.matrix_world * verts_ordered_U[i].co
2605 vert_num_in_spline += 1
2607 for t in range(0, len(sketched_splines_parsed)):
2608 ob_ctrl_pts.data.vertices.add(1)
2609 v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2610 v.co = sketched_splines_parsed[t][i]
2612 if vert_num_in_spline > 1:
2613 ob_ctrl_pts.data.edges.add(1)
2614 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = \
2615 len(ob_ctrl_pts.data.vertices) - 2
2616 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = \
2617 len(ob_ctrl_pts.data.vertices) - 1
2619 if t == 0:
2620 first_verts.append(v.index)
2622 if t == 1:
2623 second_verts.append(v.index)
2625 if t == len(sketched_splines_parsed) - 1:
2626 last_verts.append(v.index)
2628 last_v = v
2629 vert_num_in_spline += 1
2631 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2632 ob_ctrl_pts.select = True
2633 bpy.context.scene.objects.active = ob_ctrl_pts
2635 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2636 bpy.ops.mesh.select_all(action='DESELECT')
2637 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2639 # Determine which loops-U will be "Cyclic"
2640 for i in range(0, len(first_verts)):
2641 # When there is Cyclic Cross there is no need of
2642 # Automatic Join, (and there are at least three strokes)
2643 if self.automatic_join and not self.cyclic_cross and \
2644 selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3:
2646 v = ob_ctrl_pts.data.vertices
2647 first_point_co = v[first_verts[i]].co
2648 second_point_co = v[second_verts[i]].co
2649 last_point_co = v[last_verts[i]].co
2651 # Coordinates of the point in the center of both the first and last verts.
2652 verts_center_co = [
2653 (first_point_co[0] + last_point_co[0]) / 2,
2654 (first_point_co[1] + last_point_co[1]) / 2,
2655 (first_point_co[2] + last_point_co[2]) / 2
2657 vec_A = second_point_co - first_point_co
2658 vec_B = second_point_co - Vector(verts_center_co)
2660 # Calculate the length of the first segment of the loop,
2661 # and the length it would have after moving the first vert
2662 # to the middle position between first and last
2663 length_original = (second_point_co - first_point_co).length
2664 length_target = (second_point_co - Vector(verts_center_co)).length
2666 angle = vec_A.angle(vec_B) / pi
2668 # If the target length doesn't stretch too much, and the
2669 # its angle doesn't change to much either
2670 if length_target <= length_original * 1.03 * self.join_stretch_factor and \
2671 angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists:
2673 cyclic_loops_U.append(True)
2674 # Move the first vert to the center coordinates
2675 ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
2676 # Select the last verts from Cyclic loops, for later deletion all at once
2677 v[last_verts[i]].select = True
2678 else:
2679 cyclic_loops_U.append(False)
2680 else:
2681 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2682 if self.cyclic_cross and not self.selection_U_exists and not \
2683 ((self.selection_V_exists and not self.selection_V_is_closed) or
2684 (self.selection_V2_exists and not self.selection_V2_is_closed)):
2686 cyclic_loops_U.append(True)
2687 else:
2688 cyclic_loops_U.append(False)
2690 # The cyclic_loops_U list needs to be reversed.
2691 cyclic_loops_U.reverse()
2693 # Delete the previously selected (last_)verts.
2694 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2695 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
2696 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2698 # Create curves from control points.
2699 bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
2700 ob_curves_surf = bpy.context.scene.objects.active
2701 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2702 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2703 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2705 # Make Cyclic the splines designated as Cyclic.
2706 for i in range(0, len(cyclic_loops_U)):
2707 ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
2709 # Get the coords of all points on first loop-U, for later comparison with its
2710 # subdivided version, to know which points of the loops-U are crossed by the
2711 # original strokes. The indices will be the same for the other loops-U
2712 if self.loops_on_strokes:
2713 coords_loops_U_control_points = []
2714 for p in ob_ctrl_pts.data.splines[0].bezier_points:
2715 coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
2717 tuple(coords_loops_U_control_points)
2719 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2720 if self.loops_on_strokes and not self.selection_V_exists:
2721 edges_V_count = len(self.main_splines.data.splines) * self.edges_V
2722 else:
2723 edges_V_count = len(edges_proportions_V)
2725 # The Follow precision will vary depending on the number of Follow face-loops
2726 precision_multiplier = round(2 + (edges_V_count / 15))
2727 curve_cuts = bpy.context.scene.bsurfaces.SURFSK_precision * precision_multiplier
2729 # Subdivide the curves
2730 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=curve_cuts)
2732 # The verts position shifting that happens with splines subdivision.
2733 # For later reorder splines points
2734 verts_position_shift = curve_cuts + 1
2735 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2737 # Reorder coordinates of the points of each spline to put the first point of
2738 # the spline starting at the position it was the first point before sudividing
2739 # the curve. And make a new curve object per spline (to handle memory better later)
2740 splines_U_objects = []
2741 for i in range(len(ob_curves_surf.data.splines)):
2742 spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
2743 ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
2744 bpy.context.scene.objects.link(ob_spline_U)
2746 spline_U_curve.dimensions = "3D"
2748 # Add points to the spline in the new curve object
2749 ob_spline_U.data.splines.new('BEZIER')
2750 for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
2751 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2752 if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
2753 point_index = t + verts_position_shift
2754 else:
2755 point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
2756 else:
2757 point_index = t
2758 # to avoid adding the first point since it's added when the spline is created
2759 if t > 0:
2760 ob_spline_U.data.splines[0].bezier_points.add(1)
2761 ob_spline_U.data.splines[0].bezier_points[t].co = \
2762 ob_curves_surf.data.splines[i].bezier_points[point_index].co
2764 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2765 # Add a last point at the same location as the first one
2766 ob_spline_U.data.splines[0].bezier_points.add(1)
2767 ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = \
2768 ob_spline_U.data.splines[0].bezier_points[0].co
2769 else:
2770 ob_spline_U.data.splines[0].use_cyclic_u = False
2772 splines_U_objects.append(ob_spline_U)
2773 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2774 ob_spline_U.select = True
2775 bpy.context.scene.objects.active = ob_spline_U
2777 # When option "Loops on strokes" is active each "Cross" loop will have
2778 # its own proportions according to where the original strokes "touch" them
2779 if self.loops_on_strokes:
2780 # Get the indices of points where the original strokes "touch" loops-U
2781 points_U_crossed_by_strokes = []
2782 for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
2783 bp = splines_U_objects[0].data.splines[0].bezier_points[i]
2784 if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
2785 points_U_crossed_by_strokes.append(i)
2787 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2788 edge_order_number_for_splines = {}
2789 if self.selection_V_exists:
2790 # For two-connected selections add a first hypothetic stroke at the beginning.
2791 if selection_type == "TWO_CONNECTED":
2792 edge_order_number_for_splines[0] = 0
2794 for i in range(len(self.main_splines.data.splines)):
2795 sp = self.main_splines.data.splines[i]
2796 v_idx, dist_temp = self.shortest_distance(
2797 self.main_object,
2798 sp.bezier_points[0].co,
2799 verts_ordered_V_indices
2801 # Get the position (edges count) of the vert v_idx in the selected chain V
2802 edge_idx_in_chain = verts_ordered_V_indices.index(v_idx)
2804 # For two-connected selections the strokes go after the
2805 # hypothetic stroke added before, so the index adds one per spline
2806 if selection_type == "TWO_CONNECTED":
2807 spline_number = i + 1
2808 else:
2809 spline_number = i
2811 edge_order_number_for_splines[spline_number] = edge_idx_in_chain
2813 # Get the first and last verts indices for later comparison
2814 if i == 0:
2815 first_v_idx = v_idx
2816 elif i == len(self.main_splines.data.splines) - 1:
2817 last_v_idx = v_idx
2819 if self.selection_V_is_closed:
2820 # If there is no last stroke on the last vertex (same as first vertex),
2821 # add a hypothetic spline at last vert order
2822 if first_v_idx != last_v_idx:
2823 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2824 len(verts_ordered_V_indices) - 1
2825 else:
2826 if self.cyclic_cross:
2827 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2828 len(verts_ordered_V_indices) - 2
2829 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2830 len(verts_ordered_V_indices) - 1
2831 else:
2832 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2833 len(verts_ordered_V_indices) - 1
2835 # Get the coords of the points distributed along the
2836 # "crossing curves", with appropriate proportions-V
2837 surface_splines_parsed = []
2838 for i in range(len(splines_U_objects)):
2839 sp_ob = splines_U_objects[i]
2840 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2841 if self.loops_on_strokes:
2842 # Segments distances from stroke to stroke
2843 dist = 0
2844 full_dist = 0
2845 segments_distances = []
2846 for t in range(len(sp_ob.data.splines[0].bezier_points)):
2847 bp = sp_ob.data.splines[0].bezier_points[t]
2849 if t == 0:
2850 last_p = bp.co
2851 else:
2852 actual_p = bp.co
2853 dist += (last_p - actual_p).length
2855 if t in points_U_crossed_by_strokes:
2856 segments_distances.append(dist)
2857 full_dist += dist
2859 dist = 0
2861 last_p = actual_p
2863 # Calculate Proportions.
2864 used_edges_proportions_V = []
2865 for t in range(len(segments_distances)):
2866 if self.selection_V_exists:
2867 if t == 0:
2868 order_number_last_stroke = 0
2870 segment_edges_length_V = 0
2871 segment_edges_length_V2 = 0
2872 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2873 segment_edges_length_V += edges_lengths_V[order]
2874 if self.selection_V2_exists:
2875 segment_edges_length_V2 += edges_lengths_V2[order]
2877 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2878 # Calculate each "sub-segment" (the ones between each stroke) length
2879 if self.selection_V2_exists:
2880 proportion_sub_seg = (edges_lengths_V2[order] -
2881 ((edges_lengths_V2[order] - edges_lengths_V[order]) /
2882 len(splines_U_objects) * i)) / (segment_edges_length_V2 -
2883 (segment_edges_length_V2 - segment_edges_length_V) /
2884 len(splines_U_objects) * i)
2886 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2887 else:
2888 proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
2889 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2891 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2893 order_number_last_stroke = edge_order_number_for_splines[t + 1]
2895 else:
2896 for c in range(self.edges_V):
2897 # Calculate each "sub-segment" (the ones between each stroke) length
2898 sub_seg_dist = segments_distances[t] / self.edges_V
2899 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2901 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2902 surface_splines_parsed.append(actual_spline[0])
2904 else:
2905 if self.selection_V2_exists:
2906 used_edges_proportions_V = []
2907 for p in range(len(edges_proportions_V)):
2908 used_edges_proportions_V.append(
2909 edges_proportions_V2[p] -
2910 ((edges_proportions_V2[p] -
2911 edges_proportions_V[p]) / len(splines_U_objects) * i)
2913 else:
2914 used_edges_proportions_V = edges_proportions_V
2916 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2917 surface_splines_parsed.append(actual_spline[0])
2919 # Set the verts of the first and last splines to the locations
2920 # of the respective verts in the selections
2921 if self.selection_V_exists:
2922 for i in range(0, len(surface_splines_parsed[0])):
2923 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = \
2924 self.main_object.matrix_world * verts_ordered_V[i].co
2926 if selection_type == "TWO_NOT_CONNECTED":
2927 if self.selection_V2_exists:
2928 for i in range(0, len(surface_splines_parsed[0])):
2929 surface_splines_parsed[0][i] = self.main_object.matrix_world * verts_ordered_V2[i].co
2931 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2932 # merge the verts of the tips of the loops when they are "near enough"
2933 if self.automatic_join and selection_type != "TWO_CONNECTED":
2934 # Join the tips of "Follow" loops that are near enough and must be "closed"
2935 if not self.selection_V_exists and len(edges_proportions_U) >= 3:
2936 for i in range(len(surface_splines_parsed[0])):
2937 sp = surface_splines_parsed
2938 loop_segment_dist = (sp[0][i] - sp[1][i]).length
2939 full_loop_dist = loop_segment_dist * self.edges_U
2941 verts_middle_position_co = [
2942 (sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2,
2943 (sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2,
2944 (sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2
2946 points_original = []
2947 points_original.append(sp[1][i])
2948 points_original.append(sp[0][i])
2950 points_target = []
2951 points_target.append(sp[1][i])
2952 points_target.append(Vector(verts_middle_position_co))
2954 vec_A = points_original[0] - points_original[1]
2955 vec_B = points_target[0] - points_target[1]
2956 # check for zero angles, not sure if it is a great fix
2957 if vec_A.length != 0 and vec_B.length != 0:
2958 angle = vec_A.angle(vec_B) / pi
2959 edge_new_length = (Vector(verts_middle_position_co) - sp[1][i]).length
2960 else:
2961 angle = 0
2962 edge_new_length = 0
2964 # If after moving the verts to the middle point, the segment doesn't stretch too much
2965 if edge_new_length <= loop_segment_dist * 1.5 * \
2966 self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor:
2968 # Avoid joining when the actual loop must be merged with the original mesh
2969 if not (self.selection_U_exists and i == 0) and \
2970 not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1):
2972 # Change the coords of both verts to the middle position
2973 surface_splines_parsed[0][i] = verts_middle_position_co
2974 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
2976 # Delete object with control points and object from grease pencil conversion
2977 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2978 ob_ctrl_pts.select = True
2979 bpy.context.scene.objects.active = ob_ctrl_pts
2981 bpy.ops.object.delete()
2983 for sp_ob in splines_U_objects:
2984 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2985 sp_ob.select = True
2986 bpy.context.scene.objects.active = sp_ob
2988 bpy.ops.object.delete()
2990 # Generate surface
2992 # Get all verts coords
2993 all_surface_verts_co = []
2994 for i in range(0, len(surface_splines_parsed)):
2995 # Get coords of all verts and make a list with them
2996 for pt_co in surface_splines_parsed[i]:
2997 all_surface_verts_co.append(pt_co)
2999 # Define verts for each face
3000 all_surface_faces = []
3001 for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
3002 if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
3003 all_surface_faces.append(
3004 [i + 1, i, i + len(surface_splines_parsed[0]),
3005 i + len(surface_splines_parsed[0]) + 1]
3007 # Build the mesh
3008 surf_me_name = "SURFSKIO_surface"
3009 me_surf = bpy.data.meshes.new(surf_me_name)
3011 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
3013 me_surf.update()
3015 ob_surface = bpy.data.objects.new(surf_me_name, me_surf)
3016 bpy.context.scene.objects.link(ob_surface)
3018 # Select all the "unselected but participating" verts, from closed selection
3019 # or double selections with middle-vertex, for later join with remove doubles
3020 for v_idx in single_unselected_verts:
3021 self.main_object.data.vertices[v_idx].select = True
3023 # Join the new mesh to the main object
3024 ob_surface.select = True
3025 self.main_object.select = True
3026 bpy.context.scene.objects.active = self.main_object
3028 bpy.ops.object.join('INVOKE_REGION_WIN')
3030 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3032 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
3033 bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
3034 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
3036 return{'FINISHED'}
3038 def execute(self, context):
3040 bpy.context.preferences.edit.use_global_undo = False
3042 if not self.is_fill_faces:
3043 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3044 value='True, False, False')
3046 # Build splines from the "last saved splines".
3047 last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
3048 self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
3049 bpy.context.scene.objects.link(self.main_splines)
3051 last_saved_curve.dimensions = "3D"
3053 for sp in self.last_strokes_splines_coords:
3054 spline = self.main_splines.data.splines.new('BEZIER')
3055 # less one because one point is added when the spline is created
3056 spline.bezier_points.add(len(sp) - 1)
3057 for p in range(0, len(sp)):
3058 spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
3060 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3062 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3063 self.main_splines.select = True
3064 bpy.context.scene.objects.active = self.main_splines
3066 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3068 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3069 # Important to make it vector first and then automatic, otherwise the
3070 # tips handles get too big and distort the shrinkwrap results later
3071 bpy.ops.curve.handle_type_set(type='VECTOR')
3072 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3073 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3074 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3076 self.main_splines.name = "SURFSKIO_temp_strokes"
3078 if self.is_crosshatch:
3079 strokes_for_crosshatch = True
3080 strokes_for_rectangular_surface = False
3081 else:
3082 strokes_for_rectangular_surface = True
3083 strokes_for_crosshatch = False
3085 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3086 self.main_object.select = True
3087 bpy.context.scene.objects.active = self.main_object
3089 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3091 if strokes_for_rectangular_surface:
3092 self.rectangular_surface()
3093 elif strokes_for_crosshatch:
3094 self.crosshatch_surface_execute()
3096 # Delete main splines
3097 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3099 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3100 self.main_splines.select = True
3101 bpy.context.scene.objects.active = self.main_splines
3103 bpy.ops.object.delete()
3105 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3106 self.main_object.select = True
3107 bpy.context.scene.objects.active = self.main_object
3109 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3111 bpy.context.preferences.edit.use_global_undo = self.initial_global_undo_state
3113 return{'FINISHED'}
3115 def invoke(self, context, event):
3116 self.initial_global_undo_state = bpy.context.preferences.edit.use_global_undo
3118 self.main_object = bpy.context.scene.objects.active
3119 self.main_object_selected_verts_count = int(self.main_object.data.total_vert_sel)
3121 bpy.context.preferences.edit.use_global_undo = False
3122 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3123 value='True, False, False')
3125 # Out Edit mode and In again to make sure the actual mesh selections are being taken
3126 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3127 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3129 bsurfaces_props = bpy.context.scene.bsurfaces
3130 self.cyclic_cross = bsurfaces_props.SURFSK_cyclic_cross
3131 self.cyclic_follow = bsurfaces_props.SURFSK_cyclic_follow
3132 self.automatic_join = bsurfaces_props.SURFSK_automatic_join
3133 self.loops_on_strokes = bsurfaces_props.SURFSK_loops_on_strokes
3134 self.keep_strokes = bsurfaces_props.SURFSK_keep_strokes
3136 self.edges_U = 5
3138 if self.loops_on_strokes:
3139 self.edges_V = 1
3140 else:
3141 self.edges_V = 5
3143 self.is_fill_faces = False
3144 self.stopping_errors = False
3145 self.last_strokes_splines_coords = []
3147 # Determine the type of the strokes
3148 self.strokes_type = get_strokes_type(self.main_object)
3150 # Check if it will be used grease pencil strokes or curves
3151 # If there are strokes to be used
3152 if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE":
3153 if self.strokes_type == "GP_STROKES":
3154 # Convert grease pencil strokes to curve
3155 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3156 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3157 # XXX gpencil.convert now keep org object as active/selected, *not* newly created curve!
3158 # XXX This is far from perfect, but should work in most cases...
3159 # self.original_curve = bpy.context.object
3160 gplayer_prefix_translated = bpy.app.translations.pgettext_data('GP_Layer')
3161 for ob in bpy.context.selected_objects:
3162 if ob != bpy.context.scene.objects.active and \
3163 ob.name.startswith((gplayer_prefix_translated, 'GP_Layer')):
3164 self.original_curve = ob
3165 self.using_external_curves = False
3166 elif self.strokes_type == "EXTERNAL_CURVE":
3167 for ob in bpy.context.selected_objects:
3168 if ob != bpy.context.scene.objects.active:
3169 self.original_curve = ob
3170 self.using_external_curves = True
3172 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3174 # Make sure there are no objects left from erroneous
3175 # executions of this operator, with the reserved names used here
3176 for o in bpy.data.objects:
3177 if o.name.find("SURFSKIO_") != -1:
3178 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3179 o.select = True
3180 bpy.context.scene.objects.active = o
3182 bpy.ops.object.delete()
3184 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3185 self.original_curve.select = True
3186 bpy.context.scene.objects.active = self.original_curve
3188 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3190 self.temporary_curve = bpy.context.scene.objects.active
3192 # Deselect all points of the curve
3193 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3194 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3195 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3197 # Delete splines with only a single isolated point
3198 for i in range(len(self.temporary_curve.data.splines)):
3199 sp = self.temporary_curve.data.splines[i]
3201 if len(sp.bezier_points) == 1:
3202 sp.bezier_points[0].select_control_point = True
3204 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3205 bpy.ops.curve.delete(type='VERT')
3206 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3208 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3209 self.temporary_curve.select = True
3210 bpy.context.scene.objects.active = self.temporary_curve
3212 # Set a minimum number of points for crosshatch
3213 minimum_points_num = 15
3215 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3216 # Check if the number of points of each curve has at least the number of points
3217 # of minimum_points_num, which is a bit more than the face-loops limit.
3218 # If not, subdivide to reach at least that number of points
3219 for i in range(len(self.temporary_curve.data.splines)):
3220 sp = self.temporary_curve.data.splines[i]
3222 if len(sp.bezier_points) < minimum_points_num:
3223 for bp in sp.bezier_points:
3224 bp.select_control_point = True
3226 if (len(sp.bezier_points) - 1) != 0:
3227 # Formula to get the number of cuts that will make a curve
3228 # of N number of points have near to "minimum_points_num"
3229 # points, when subdividing with this number of cuts
3230 subdivide_cuts = int(
3231 (minimum_points_num - len(sp.bezier_points)) /
3232 (len(sp.bezier_points) - 1)
3233 ) + 1
3234 else:
3235 subdivide_cuts = 0
3237 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3238 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3240 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3242 # Detect if the strokes are a crosshatch and do it if it is
3243 self.crosshatch_surface_invoke(self.temporary_curve)
3245 if not self.is_crosshatch:
3246 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3247 self.temporary_curve.select = True
3248 bpy.context.scene.objects.active = self.temporary_curve
3250 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3252 # Set a minimum number of points for rectangular surfaces
3253 minimum_points_num = 60
3255 # Check if the number of points of each curve has at least the number of points
3256 # of minimum_points_num, which is a bit more than the face-loops limit.
3257 # If not, subdivide to reach at least that number of points
3258 for i in range(len(self.temporary_curve.data.splines)):
3259 sp = self.temporary_curve.data.splines[i]
3261 if len(sp.bezier_points) < minimum_points_num:
3262 for bp in sp.bezier_points:
3263 bp.select_control_point = True
3265 if (len(sp.bezier_points) - 1) != 0:
3266 # Formula to get the number of cuts that will make a curve of
3267 # N number of points have near to "minimum_points_num" points,
3268 # when subdividing with this number of cuts
3269 subdivide_cuts = int(
3270 (minimum_points_num - len(sp.bezier_points)) /
3271 (len(sp.bezier_points) - 1)
3272 ) + 1
3273 else:
3274 subdivide_cuts = 0
3276 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3277 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3279 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3281 # Save coordinates of the actual strokes (as the "last saved splines")
3282 for sp_idx in range(len(self.temporary_curve.data.splines)):
3283 self.last_strokes_splines_coords.append([])
3284 for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
3285 coords = self.temporary_curve.matrix_world * \
3286 self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
3287 self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
3289 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3290 for sp_idx in range(len(self.temporary_curve.data.splines)):
3291 if self.temporary_curve.data.splines[sp_idx].use_cyclic_u is True:
3292 first_p_co = self.last_strokes_splines_coords[sp_idx][0]
3293 last_p_co = self.last_strokes_splines_coords[sp_idx][
3294 len(self.last_strokes_splines_coords[sp_idx]) - 1
3296 target_co = [
3297 (first_p_co[0] + last_p_co[0]) / 2,
3298 (first_p_co[1] + last_p_co[1]) / 2,
3299 (first_p_co[2] + last_p_co[2]) / 2
3302 self.last_strokes_splines_coords[sp_idx][0] = target_co
3303 self.last_strokes_splines_coords[sp_idx][
3304 len(self.last_strokes_splines_coords[sp_idx]) - 1
3305 ] = target_co
3306 tuple(self.last_strokes_splines_coords)
3308 # Estimation of the average length of the segments between
3309 # each point of the grease pencil strokes.
3310 # Will be useful to determine whether a curve should be made "Cyclic"
3311 segments_lengths_sum = 0
3312 segments_count = 0
3313 random_spline = self.temporary_curve.data.splines[0].bezier_points
3314 for i in range(0, len(random_spline)):
3315 if i != 0 and len(random_spline) - 1 >= i:
3316 segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
3317 segments_count += 1
3319 self.average_gp_segment_length = segments_lengths_sum / segments_count
3321 # Delete temporary strokes curve object
3322 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3323 self.temporary_curve.select = True
3324 bpy.context.scene.objects.active = self.temporary_curve
3326 bpy.ops.object.delete()
3328 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3329 self.main_object.select = True
3330 bpy.context.scene.objects.active = self.main_object
3332 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3334 self.execute(context)
3335 # Set again since "execute()" will turn it again to its initial value
3336 bpy.context.preferences.edit.use_global_undo = False
3338 # If "Keep strokes" option is not active, delete original strokes curve object
3339 if (not self.stopping_errors and not self.keep_strokes) or self.is_crosshatch:
3340 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3341 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3342 self.original_curve.select = True
3343 bpy.context.scene.objects.active = self.original_curve
3345 bpy.ops.object.delete()
3347 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3348 self.main_object.select = True
3349 bpy.context.scene.objects.active = self.main_object
3351 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3353 # Delete grease pencil strokes
3354 if self.strokes_type == "GP_STROKES" and not self.stopping_errors:
3355 bpy.ops.gpencil.active_frame_delete('INVOKE_REGION_WIN')
3357 bpy.context.preferences.edit.use_global_undo = self.initial_global_undo_state
3359 if not self.stopping_errors:
3360 return {"FINISHED"}
3361 else:
3362 return{"CANCELLED"}
3364 elif self.strokes_type == "SELECTION_ALONE":
3365 self.is_fill_faces = True
3366 created_faces_count = self.fill_with_faces(self.main_object)
3368 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3369 bpy.context.preferences.edit.use_global_undo = self.initial_global_undo_state
3371 if created_faces_count == 0:
3372 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3373 return {"CANCELLED"}
3374 else:
3375 return {"FINISHED"}
3377 bpy.context.preferences.edit.use_global_undo = self.initial_global_undo_state
3379 if self.strokes_type == "EXTERNAL_NO_CURVE":
3380 self.report({'WARNING'}, "The secondary object is not a Curve.")
3381 return{"CANCELLED"}
3383 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3384 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3385 return{"CANCELLED"}
3387 elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or \
3388 self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3390 self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3391 return{"CANCELLED"}
3393 elif self.strokes_type == "NO_STROKES":
3394 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3395 return{"CANCELLED"}
3397 elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
3398 self.report({'WARNING'}, "All splines must be Bezier.")
3399 return{"CANCELLED"}
3401 else:
3402 return{"CANCELLED"}
3405 # Edit strokes operator
3406 class GPENCIL_OT_SURFSK_edit_strokes(Operator):
3407 bl_idname = "gpencil.surfsk_edit_strokes"
3408 bl_label = "Bsurfaces edit strokes"
3409 bl_description = "Edit the grease pencil strokes or curves used"
3411 def execute(self, context):
3412 # Determine the type of the strokes
3413 self.strokes_type = get_strokes_type(self.main_object)
3414 # Check if strokes are grease pencil strokes or a curves object
3415 selected_objs = bpy.context.selected_objects
3416 if self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3417 for ob in selected_objs:
3418 if ob != bpy.context.scene.objects.active:
3419 curve_ob = ob
3421 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3423 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3424 curve_ob.select = True
3425 bpy.context.scene.objects.active = curve_ob
3427 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3428 elif self.strokes_type == "GP_STROKES" or self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION":
3429 # Convert grease pencil strokes to curve
3430 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3431 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3432 for ob in bpy.context.selected_objects:
3433 if ob != bpy.context.scene.objects.active and ob.name.startswith("GP_Layer"):
3434 ob_gp_strokes = ob
3436 # ob_gp_strokes = bpy.context.object
3438 # Delete grease pencil strokes
3439 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3440 self.main_object.select = True
3441 bpy.context.scene.objects.active = self.main_object
3443 bpy.ops.gpencil.active_frame_delete('INVOKE_REGION_WIN')
3445 # Clean up curves
3446 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3447 ob_gp_strokes.select = True
3448 bpy.context.scene.objects.active = ob_gp_strokes
3450 curve_crv = ob_gp_strokes.data
3451 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3452 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type="BEZIER")
3453 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type="AUTOMATIC")
3454 curve_crv.show_handles = False
3455 curve_crv.show_normal_face = False
3457 elif self.strokes_type == "EXTERNAL_NO_CURVE":
3458 self.report({'WARNING'}, "The secondary object is not a Curve.")
3459 return{"CANCELLED"}
3461 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3462 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3463 return{"CANCELLED"}
3465 elif self.strokes_type == "NO_STROKES" or self.strokes_type == "SELECTION_ALONE":
3466 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3467 return{"CANCELLED"}
3469 else:
3470 return{"CANCELLED"}
3472 def invoke(self, context, event):
3473 self.main_object = bpy.context.object
3474 self.execute(context)
3476 return {"FINISHED"}
3479 class CURVE_OT_SURFSK_reorder_splines(Operator):
3480 bl_idname = "curve.surfsk_reorder_splines"
3481 bl_label = "Bsurfaces reorder splines"
3482 bl_description = "Defines the order of the splines by using grease pencil strokes"
3483 bl_options = {'REGISTER', 'UNDO'}
3485 def execute(self, context):
3486 objects_to_delete = []
3487 # Convert grease pencil strokes to curve.
3488 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3489 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3490 for ob in bpy.context.selected_objects:
3491 if ob != bpy.context.scene.objects.active and ob.name.startswith("GP_Layer"):
3492 GP_strokes_curve = ob
3494 # GP_strokes_curve = bpy.context.object
3495 objects_to_delete.append(GP_strokes_curve)
3497 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3498 GP_strokes_curve.select = True
3499 bpy.context.scene.objects.active = GP_strokes_curve
3501 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3502 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3503 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=100)
3504 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3506 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3507 GP_strokes_mesh = bpy.context.object
3508 objects_to_delete.append(GP_strokes_mesh)
3510 GP_strokes_mesh.data.resolution_u = 1
3511 bpy.ops.object.convert(target='MESH', keep_original=False)
3513 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3514 self.main_curve.select = True
3515 bpy.context.scene.objects.active = self.main_curve
3517 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3518 curves_duplicate_1 = bpy.context.object
3519 objects_to_delete.append(curves_duplicate_1)
3521 minimum_points_num = 500
3523 # Some iterations since the subdivision operator
3524 # has a limit of 100 subdivisions per iteration
3525 for x in range(round(minimum_points_num / 100)):
3526 # Check if the number of points of each curve has at least the number of points
3527 # of minimum_points_num. If not, subdivide to reach at least that number of points
3528 for i in range(len(curves_duplicate_1.data.splines)):
3529 sp = curves_duplicate_1.data.splines[i]
3531 if len(sp.bezier_points) < minimum_points_num:
3532 for bp in sp.bezier_points:
3533 bp.select_control_point = True
3535 if (len(sp.bezier_points) - 1) != 0:
3536 # Formula to get the number of cuts that will make a curve of N
3537 # number of points have near to "minimum_points_num" points,
3538 # when subdividing with this number of cuts
3539 subdivide_cuts = int(
3540 (minimum_points_num - len(sp.bezier_points)) /
3541 (len(sp.bezier_points) - 1)
3542 ) + 1
3543 else:
3544 subdivide_cuts = 0
3546 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3547 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3548 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3549 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3551 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3552 curves_duplicate_2 = bpy.context.object
3553 objects_to_delete.append(curves_duplicate_2)
3555 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3556 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3557 curves_duplicate_2.select = True
3558 bpy.context.scene.objects.active = curves_duplicate_2
3560 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
3561 curves_duplicate_2.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX"
3562 curves_duplicate_2.modifiers["Shrinkwrap"].target = GP_strokes_mesh
3563 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier='Shrinkwrap')
3565 # Get the distance of each vert from its original position to its position with Shrinkwrap
3566 nearest_points_coords = {}
3567 for st_idx in range(len(curves_duplicate_1.data.splines)):
3568 for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
3569 bp_1_co = curves_duplicate_1.matrix_world * \
3570 curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
3572 bp_2_co = curves_duplicate_2.matrix_world * \
3573 curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
3575 if bp_idx == 0:
3576 shortest_dist = (bp_1_co - bp_2_co).length
3577 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3578 "%.4f" % bp_2_co[1],
3579 "%.4f" % bp_2_co[2])
3581 dist = (bp_1_co - bp_2_co).length
3583 if dist < shortest_dist:
3584 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
3585 "%.4f" % bp_2_co[1],
3586 "%.4f" % bp_2_co[2])
3587 shortest_dist = dist
3589 # Get all coords of GP strokes points, for comparison
3590 GP_strokes_coords = []
3591 for st_idx in range(len(GP_strokes_curve.data.splines)):
3592 GP_strokes_coords.append(
3593 [("%.4f" % x if "%.4f" % x != "-0.00" else "0.00",
3594 "%.4f" % y if "%.4f" % y != "-0.00" else "0.00",
3595 "%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for
3596 x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]]
3599 # Check the point of the GP strokes with the same coords as
3600 # the nearest points of the curves (with shrinkwrap)
3602 # Dictionary with GP stroke index as index, and a list as value.
3603 # The list has as index the point index of the GP stroke
3604 # nearest to the spline, and as value the spline index
3605 GP_connection_points = {}
3606 for gp_st_idx in range(len(GP_strokes_coords)):
3607 GPvert_spline_relationship = {}
3609 for splines_st_idx in range(len(nearest_points_coords)):
3610 if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
3611 GPvert_spline_relationship[
3612 GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])
3613 ] = splines_st_idx
3615 GP_connection_points[gp_st_idx] = GPvert_spline_relationship
3617 # Get the splines new order
3618 splines_new_order = []
3619 for i in GP_connection_points:
3620 dict_keys = sorted(GP_connection_points[i].keys()) # Sort dictionaries by key
3622 for k in dict_keys:
3623 splines_new_order.append(GP_connection_points[i][k])
3625 # Reorder
3626 curve_original_name = self.main_curve.name
3628 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3629 self.main_curve.select = True
3630 bpy.context.scene.objects.active = self.main_curve
3632 self.main_curve.name = "SURFSKIO_CRV_ORD"
3634 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3635 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3636 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3638 for sp_idx in range(len(self.main_curve.data.splines)):
3639 self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
3641 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3642 bpy.ops.curve.separate('EXEC_REGION_WIN')
3643 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3645 # Get the names of the separated splines objects in the original order
3646 splines_unordered = {}
3647 for o in bpy.data.objects:
3648 if o.name.find("SURFSKIO_CRV_ORD") != -1:
3649 spline_order_string = o.name.partition(".")[2]
3651 if spline_order_string != "" and int(spline_order_string) > 0:
3652 spline_order_index = int(spline_order_string) - 1
3653 splines_unordered[spline_order_index] = o.name
3655 # Join all splines objects in final order
3656 for order_idx in splines_new_order:
3657 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3658 bpy.data.objects[splines_unordered[order_idx]].select = True
3659 bpy.data.objects["SURFSKIO_CRV_ORD"].select = True
3660 bpy.context.scene.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
3662 bpy.ops.object.join('INVOKE_REGION_WIN')
3664 # Go back to the original name of the curves object.
3665 bpy.context.object.name = curve_original_name
3667 # Delete all unused objects
3668 for o in objects_to_delete:
3669 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3670 o.select = True
3671 bpy.context.scene.objects.active = o
3673 bpy.ops.object.delete()
3675 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3676 bpy.data.objects[curve_original_name].select = True
3677 bpy.context.scene.objects.active = bpy.data.objects[curve_original_name]
3679 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3680 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3682 bpy.ops.gpencil.active_frame_delete('INVOKE_REGION_WIN')
3684 return {"FINISHED"}
3686 def invoke(self, context, event):
3687 self.main_curve = bpy.context.object
3688 there_are_GP_strokes = False
3690 try:
3691 # Get the active grease pencil layer
3692 strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
3694 if strokes_num > 0:
3695 there_are_GP_strokes = True
3696 except:
3697 pass
3699 if there_are_GP_strokes:
3700 self.execute(context)
3701 self.report({'INFO'}, "Splines have been reordered")
3702 else:
3703 self.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
3705 return {"FINISHED"}
3708 class CURVE_OT_SURFSK_first_points(Operator):
3709 bl_idname = "curve.surfsk_first_points"
3710 bl_label = "Bsurfaces set first points"
3711 bl_description = "Set the selected points as the first point of each spline"
3712 bl_options = {'REGISTER', 'UNDO'}
3714 def execute(self, context):
3715 splines_to_invert = []
3717 # Check non-cyclic splines to invert
3718 for i in range(len(self.main_curve.data.splines)):
3719 b_points = self.main_curve.data.splines[i].bezier_points
3721 if i not in self.cyclic_splines: # Only for non-cyclic splines
3722 if b_points[len(b_points) - 1].select_control_point:
3723 splines_to_invert.append(i)
3725 # Reorder points of cyclic splines, and set all handles to "Automatic"
3727 # Check first selected point
3728 cyclic_splines_new_first_pt = {}
3729 for i in self.cyclic_splines:
3730 sp = self.main_curve.data.splines[i]
3732 for t in range(len(sp.bezier_points)):
3733 bp = sp.bezier_points[t]
3734 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
3735 cyclic_splines_new_first_pt[i] = t
3736 break # To take only one if there are more
3738 # Reorder
3739 for spline_idx in cyclic_splines_new_first_pt:
3740 sp = self.main_curve.data.splines[spline_idx]
3742 spline_old_coords = []
3743 for bp_old in sp.bezier_points:
3744 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
3746 left_handle_type = str(bp_old.handle_left_type)
3747 left_handle_length = float(bp_old.handle_left.length)
3748 left_handle_xyz = (
3749 float(bp_old.handle_left.x),
3750 float(bp_old.handle_left.y),
3751 float(bp_old.handle_left.z)
3753 right_handle_type = str(bp_old.handle_right_type)
3754 right_handle_length = float(bp_old.handle_right.length)
3755 right_handle_xyz = (
3756 float(bp_old.handle_right.x),
3757 float(bp_old.handle_right.y),
3758 float(bp_old.handle_right.z)
3760 spline_old_coords.append(
3761 [coords, left_handle_type,
3762 right_handle_type, left_handle_length,
3763 right_handle_length, left_handle_xyz,
3764 right_handle_xyz]
3767 for t in range(len(sp.bezier_points)):
3768 bp = sp.bezier_points
3770 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
3771 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
3772 else:
3773 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
3775 bp[t].co = Vector(spline_old_coords[new_index][0])
3777 bp[t].handle_left.length = spline_old_coords[new_index][3]
3778 bp[t].handle_right.length = spline_old_coords[new_index][4]
3780 bp[t].handle_left_type = "FREE"
3781 bp[t].handle_right_type = "FREE"
3783 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
3784 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
3785 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
3787 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
3788 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
3789 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
3791 bp[t].handle_left_type = spline_old_coords[new_index][1]
3792 bp[t].handle_right_type = spline_old_coords[new_index][2]
3794 # Invert the non-cyclic splines designated above
3795 for i in range(len(splines_to_invert)):
3796 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3798 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3799 self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
3800 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3802 bpy.ops.curve.switch_direction()
3804 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3806 # Keep selected the first vert of each spline
3807 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3808 for i in range(len(self.main_curve.data.splines)):
3809 if not self.main_curve.data.splines[i].use_cyclic_u:
3810 bp = self.main_curve.data.splines[i].bezier_points[0]
3811 else:
3812 bp = self.main_curve.data.splines[i].bezier_points[
3813 len(self.main_curve.data.splines[i].bezier_points) - 1
3816 bp.select_control_point = True
3817 bp.select_right_handle = True
3818 bp.select_left_handle = True
3820 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3822 return {'FINISHED'}
3824 def invoke(self, context, event):
3825 self.main_curve = bpy.context.object
3827 # Check if all curves are Bezier, and detect which ones are cyclic
3828 self.cyclic_splines = []
3829 for i in range(len(self.main_curve.data.splines)):
3830 if self.main_curve.data.splines[i].type != "BEZIER":
3831 self.report({'WARNING'}, "All splines must be Bezier type")
3833 return {'CANCELLED'}
3834 else:
3835 if self.main_curve.data.splines[i].use_cyclic_u:
3836 self.cyclic_splines.append(i)
3838 self.execute(context)
3839 self.report({'INFO'}, "First points have been set")
3841 return {'FINISHED'}
3844 # Add-ons Preferences Update Panel
3846 # Define Panel classes for updating
3847 panels = (
3848 VIEW3D_PT_tools_SURFSK_mesh,
3849 VIEW3D_PT_tools_SURFSK_curve,
3853 def update_panel(self, context):
3854 message = "Bsurfaces GPL Edition: Updating Panel locations has failed"
3855 try:
3856 for panel in panels:
3857 if "bl_rna" in panel.__dict__:
3858 bpy.utils.unregister_class(panel)
3860 for panel in panels:
3861 panel.bl_category = context.preferences.addons[__name__].preferences.category
3862 bpy.utils.register_class(panel)
3864 except Exception as e:
3865 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
3866 pass
3869 class BsurfPreferences(AddonPreferences):
3870 # this must match the addon name, use '__package__'
3871 # when defining this in a submodule of a python package.
3872 bl_idname = __name__
3874 category = StringProperty(
3875 name="Tab Category",
3876 description="Choose a name for the category of the panel",
3877 default="Tools",
3878 update=update_panel
3881 def draw(self, context):
3882 layout = self.layout
3884 row = layout.row()
3885 col = row.column()
3886 col.label(text="Tab Category:")
3887 col.prop(self, "category", text="")
3890 # Properties
3891 class BsurfacesProps(PropertyGroup):
3892 SURFSK_cyclic_cross = BoolProperty(
3893 name="Cyclic Cross",
3894 description="Make cyclic the face-loops crossing the strokes",
3895 default=False
3897 SURFSK_cyclic_follow = BoolProperty(
3898 name="Cyclic Follow",
3899 description="Make cyclic the face-loops following the strokes",
3900 default=False
3902 SURFSK_keep_strokes = BoolProperty(
3903 name="Keep strokes",
3904 description="Keeps the sketched strokes or curves after adding the surface",
3905 default=False
3907 SURFSK_automatic_join = BoolProperty(
3908 name="Automatic join",
3909 description="Join automatically vertices of either surfaces "
3910 "generated by crosshatching, or from the borders of closed shapes",
3911 default=True
3913 SURFSK_loops_on_strokes = BoolProperty(
3914 name="Loops on strokes",
3915 description="Make the loops match the paths of the strokes",
3916 default=True
3918 SURFSK_precision = IntProperty(
3919 name="Precision",
3920 description="Precision level of the surface calculation",
3921 default=2,
3922 min=1,
3923 max=100
3927 classes = (
3928 VIEW3D_PT_tools_SURFSK_mesh,
3929 VIEW3D_PT_tools_SURFSK_curve,
3930 GPENCIL_OT_SURFSK_add_surface,
3931 GPENCIL_OT_SURFSK_edit_strokes,
3932 CURVE_OT_SURFSK_reorder_splines,
3933 CURVE_OT_SURFSK_first_points,
3934 BsurfPreferences,
3935 BsurfacesProps,
3939 def register():
3940 for cls in classes:
3941 bpy.utils.register_class(cls)
3943 bpy.types.Scene.bsurfaces = PointerProperty(type=BsurfacesProps)
3944 update_panel(None, bpy.context)
3947 def unregister():
3948 for cls in classes:
3949 bpy.utils.unregister_class(cls)
3951 del bpy.types.Scene.bsurfaces
3954 if __name__ == "__main__":
3955 register()