Merge branch 'blender-v2.83-release'
[blender-addons.git] / mesh_bsurfaces.py
blobc0c7a4f9c91ee205f59a1cf2cb9ff3bde21a68ef
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, Vladimir Spivak (cwolf3d)",
23 "version": (1, 7, 8),
24 "blender": (2, 80, 0),
25 "location": "View3D EditMode > Sidebar > Edit Tab",
26 "description": "Modeling and retopology tool",
27 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
28 "category": "Mesh",
32 import bpy
33 import bmesh
34 from bpy_extras import object_utils
36 import operator
37 from mathutils import Matrix, Vector
38 from mathutils.geometry import (
39 intersect_line_line,
40 intersect_point_line,
42 from math import (
43 degrees,
44 pi,
45 sqrt,
47 from bpy.props import (
48 BoolProperty,
49 FloatProperty,
50 IntProperty,
51 StringProperty,
52 PointerProperty,
53 EnumProperty,
54 FloatVectorProperty,
56 from bpy.types import (
57 Operator,
58 Panel,
59 PropertyGroup,
60 AddonPreferences,
63 # ----------------------------
64 # GLOBAL
65 global_color = [1.0, 0.0, 0.0, 0.3]
66 global_offset = 0.01
67 global_in_front = False
68 global_shade_smooth = False
69 global_show_wire = True
70 global_mesh_object = ""
71 global_gpencil_object = ""
72 global_curve_object = ""
74 # ----------------------------
75 # Panels
76 class VIEW3D_PT_tools_SURFSK_mesh(Panel):
77 bl_space_type = 'VIEW_3D'
78 bl_region_type = 'UI'
79 bl_category = 'Edit'
80 bl_label = "Bsurfaces"
82 def draw(self, context):
83 layout = self.layout
84 scn = context.scene.bsurfaces
86 col = layout.column(align=True)
87 row = layout.row()
88 row.separator()
89 col.operator("mesh.surfsk_init", text="Initialize (Add BSurface mesh)")
90 col.operator("mesh.surfsk_add_modifiers", text="Add Mirror and others modifiers")
92 col.label(text="Mesh of BSurface:")
93 col.prop(scn, "SURFSK_mesh", text="")
94 col.prop(scn, "SURFSK_mesh_color")
95 col.prop(scn, "SURFSK_Shrinkwrap_offset")
96 col.prop(scn, "SURFSK_in_front")
97 col.prop(scn, "SURFSK_shade_smooth")
98 col.prop(scn, "SURFSK_show_wire")
100 col.label(text="Guide strokes:")
101 col.row().prop(scn, "SURFSK_guide", expand=True)
102 if scn.SURFSK_guide == 'GPencil':
103 col.prop(scn, "SURFSK_gpencil", text="")
104 col.separator()
105 if scn.SURFSK_guide == 'Curve':
106 col.prop(scn, "SURFSK_curve", text="")
107 col.separator()
109 col.separator()
110 col.operator("mesh.surfsk_add_surface", text="Add Surface")
111 col.operator("mesh.surfsk_edit_surface", text="Edit Surface")
113 col.separator()
114 if scn.SURFSK_guide == 'GPencil':
115 col.operator("gpencil.surfsk_add_strokes", text="Add Strokes")
116 col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
117 col.separator()
118 col.operator("gpencil.surfsk_strokes_to_curves", text="Strokes to curves")
120 if scn.SURFSK_guide == 'Annotation':
121 col.operator("gpencil.surfsk_add_annotation", text="Add Annotation")
122 col.separator()
123 col.operator("gpencil.surfsk_annotations_to_curves", text="Annotation to curves")
125 if scn.SURFSK_guide == 'Curve':
126 col.operator("curve.surfsk_edit_curve", text="Edit curve")
128 col.separator()
129 col.label(text="Initial settings:")
130 col.prop(scn, "SURFSK_edges_U")
131 col.prop(scn, "SURFSK_edges_V")
132 col.prop(scn, "SURFSK_cyclic_cross")
133 col.prop(scn, "SURFSK_cyclic_follow")
134 col.prop(scn, "SURFSK_loops_on_strokes")
135 col.prop(scn, "SURFSK_automatic_join")
136 col.prop(scn, "SURFSK_keep_strokes")
138 class VIEW3D_PT_tools_SURFSK_curve(Panel):
139 bl_space_type = 'VIEW_3D'
140 bl_region_type = 'UI'
141 bl_context = "curve_edit"
142 bl_category = 'Edit'
143 bl_label = "Bsurfaces"
145 @classmethod
146 def poll(cls, context):
147 return context.active_object
149 def draw(self, context):
150 layout = self.layout
152 col = layout.column(align=True)
153 row = layout.row()
154 row.separator()
155 col.operator("curve.surfsk_first_points", text="Set First Points")
156 col.operator("curve.switch_direction", text="Switch Direction")
157 col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
160 # ----------------------------
161 # Returns the type of strokes used
162 def get_strokes_type(context):
163 strokes_type = "NO_STROKES"
164 strokes_num = 0
166 # Check if they are annotation
167 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
168 try:
169 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
171 strokes_num = len(strokes)
173 if strokes_num > 0:
174 strokes_type = "GP_ANNOTATION"
175 except:
176 strokes_type = "NO_STROKES"
178 # Check if they are grease pencil
179 if context.scene.bsurfaces.SURFSK_guide == 'GPencil':
180 try:
181 global global_gpencil_object
182 gpencil = bpy.data.objects[global_gpencil_object]
183 strokes = gpencil.data.layers.active.active_frame.strokes
185 strokes_num = len(strokes)
187 if strokes_num > 0:
188 strokes_type = "GP_STROKES"
189 except:
190 strokes_type = "NO_STROKES"
192 # Check if they are curves, if there aren't grease pencil strokes
193 if context.scene.bsurfaces.SURFSK_guide == 'Curve':
194 try:
195 global global_curve_object
196 ob = bpy.data.objects[global_curve_object]
197 if ob.type == "CURVE":
198 strokes_type = "EXTERNAL_CURVE"
199 strokes_num = len(ob.data.splines)
201 # Check if there is any non-bezier spline
202 for i in range(len(ob.data.splines)):
203 if ob.data.splines[i].type != "BEZIER":
204 strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
205 break
207 else:
208 strokes_type = "EXTERNAL_NO_CURVE"
209 except:
210 strokes_type = "NO_STROKES"
212 # Check if they are mesh
213 try:
214 global global_mesh_object
215 self.main_object = bpy.data.objects[global_mesh_object]
216 total_vert_sel = len([v for v in self.main_object.data.vertices if v.select])
218 # Check if there is a single stroke without any selection in the object
219 if strokes_num == 1 and total_vert_sel == 0:
220 if strokes_type == "EXTERNAL_CURVE":
221 strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
222 elif strokes_type == "GP_STROKES":
223 strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"
225 if strokes_num == 0 and total_vert_sel > 0:
226 strokes_type = "SELECTION_ALONE"
227 except:
228 pass
230 return strokes_type
232 # ----------------------------
233 # Surface generator operator
234 class MESH_OT_SURFSK_add_surface(Operator):
235 bl_idname = "mesh.surfsk_add_surface"
236 bl_label = "Bsurfaces add surface"
237 bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
238 bl_options = {'REGISTER', 'UNDO'}
240 is_fill_faces: BoolProperty(
241 default=False
243 selection_U_exists: BoolProperty(
244 default=False
246 selection_V_exists: BoolProperty(
247 default=False
249 selection_U2_exists: BoolProperty(
250 default=False
252 selection_V2_exists: BoolProperty(
253 default=False
255 selection_V_is_closed: BoolProperty(
256 default=False
258 selection_U_is_closed: BoolProperty(
259 default=False
261 selection_V2_is_closed: BoolProperty(
262 default=False
264 selection_U2_is_closed: BoolProperty(
265 default=False
268 edges_U: IntProperty(
269 name="Cross",
270 description="Number of face-loops crossing the strokes",
271 default=1,
272 min=1,
273 max=200
275 edges_V: IntProperty(
276 name="Follow",
277 description="Number of face-loops following the strokes",
278 default=1,
279 min=1,
280 max=200
282 cyclic_cross: BoolProperty(
283 name="Cyclic Cross",
284 description="Make cyclic the face-loops crossing the strokes",
285 default=False
287 cyclic_follow: BoolProperty(
288 name="Cyclic Follow",
289 description="Make cyclic the face-loops following the strokes",
290 default=False
292 loops_on_strokes: BoolProperty(
293 name="Loops on strokes",
294 description="Make the loops match the paths of the strokes",
295 default=False
297 automatic_join: BoolProperty(
298 name="Automatic join",
299 description="Join automatically vertices of either surfaces generated "
300 "by crosshatching, or from the borders of closed shapes",
301 default=False
303 join_stretch_factor: FloatProperty(
304 name="Stretch",
305 description="Amount of stretching or shrinking allowed for "
306 "edges when joining vertices automatically",
307 default=1,
308 min=0,
309 max=3,
310 subtype='FACTOR'
312 keep_strokes: BoolProperty(
313 name="Keep strokes",
314 description="Keeps the sketched strokes or curves after adding the surface",
315 default=False
317 strokes_type: StringProperty()
318 initial_global_undo_state: BoolProperty()
321 def draw(self, context):
322 layout = self.layout
323 col = layout.column(align=True)
324 row = layout.row()
326 if not self.is_fill_faces:
327 row.separator()
328 if not self.is_crosshatch:
329 if not self.selection_U_exists:
330 col.prop(self, "edges_U")
331 row.separator()
333 if not self.selection_V_exists:
334 col.prop(self, "edges_V")
335 row.separator()
337 row.separator()
339 if not self.selection_U_exists:
340 if not (
341 (self.selection_V_exists and not self.selection_V_is_closed) or
342 (self.selection_V2_exists and not self.selection_V2_is_closed)
344 col.prop(self, "cyclic_cross")
346 if not self.selection_V_exists:
347 if not (
348 (self.selection_U_exists and not self.selection_U_is_closed) or
349 (self.selection_U2_exists and not self.selection_U2_is_closed)
351 col.prop(self, "cyclic_follow")
353 col.prop(self, "loops_on_strokes")
355 col.prop(self, "automatic_join")
357 if self.automatic_join:
358 row.separator()
359 col.separator()
360 row.separator()
361 col.prop(self, "join_stretch_factor")
363 col.prop(self, "keep_strokes")
365 # Get an ordered list of a chain of vertices
366 def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx,
367 first_vert_idx, middle_vertex_idx, closing_vert_idx):
368 # Order selected vertices.
369 verts_ordered = []
370 if closing_vert_idx is not None:
371 verts_ordered.append(ob.data.vertices[closing_vert_idx])
373 verts_ordered.append(ob.data.vertices[first_vert_idx])
374 prev_v = first_vert_idx
375 prev_ed = None
376 finish_while = False
377 while True:
378 edges_non_matched = 0
379 for i in all_selected_edges_idx:
380 if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and \
381 ob.data.edges[i].vertices[1] in all_selected_verts_idx:
383 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
384 prev_v = ob.data.edges[i].vertices[1]
385 prev_ed = ob.data.edges[i]
386 elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and \
387 ob.data.edges[i].vertices[0] in all_selected_verts_idx:
389 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
390 prev_v = ob.data.edges[i].vertices[0]
391 prev_ed = ob.data.edges[i]
392 else:
393 edges_non_matched += 1
395 if edges_non_matched == len(all_selected_edges_idx):
396 finish_while = True
398 if finish_while:
399 break
401 if closing_vert_idx is not None:
402 verts_ordered.append(ob.data.vertices[closing_vert_idx])
404 if middle_vertex_idx is not None:
405 verts_ordered.append(ob.data.vertices[middle_vertex_idx])
406 verts_ordered.reverse()
408 return tuple(verts_ordered)
410 # Calculates length of a chain of points.
411 def get_chain_length(self, object, verts_ordered):
412 matrix = object.matrix_world
414 edges_lengths = []
415 edges_lengths_sum = 0
416 for i in range(0, len(verts_ordered)):
417 if i == 0:
418 prev_v_co = matrix @ verts_ordered[i].co
419 else:
420 v_co = matrix @ verts_ordered[i].co
422 v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
423 edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
425 edges_lengths.append(edge_length)
426 edges_lengths_sum += edge_length
428 prev_v_co = v_co
430 return edges_lengths, edges_lengths_sum
432 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
433 def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
434 edges_proportions = []
435 if use_boundaries:
436 verts_count = 1
437 for l in edges_lengths:
438 edges_proportions.append(l / edges_lengths_sum)
439 verts_count += 1
440 else:
441 verts_count = 1
442 for _n in range(0, fixed_edges_num):
443 edges_proportions.append(1 / fixed_edges_num)
444 verts_count += 1
446 return edges_proportions
448 # Calculates the angle between two pairs of points in space
449 def orientation_difference(self, points_A_co, points_B_co):
450 # each parameter should be a list with two elements,
451 # and each element should be a x,y,z coordinate
452 vec_A = points_A_co[0] - points_A_co[1]
453 vec_B = points_B_co[0] - points_B_co[1]
455 angle = vec_A.angle(vec_B)
457 if angle > 0.5 * pi:
458 angle = abs(angle - pi)
460 return angle
462 # Calculate the which vert of verts_idx list is the nearest one
463 # to the point_co coordinates, and the distance
464 def shortest_distance(self, object, point_co, verts_idx):
465 matrix = object.matrix_world
467 for i in range(0, len(verts_idx)):
468 dist = (point_co - matrix @ object.data.vertices[verts_idx[i]].co).length
469 if i == 0:
470 prev_dist = dist
471 nearest_vert_idx = verts_idx[i]
472 shortest_dist = dist
474 if dist < prev_dist:
475 prev_dist = dist
476 nearest_vert_idx = verts_idx[i]
477 shortest_dist = dist
479 return nearest_vert_idx, shortest_dist
481 # Returns the index of the opposite vert tip in a chain, given a vert tip index
482 # as parameter, and a multidimentional list with all pairs of tips
483 def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
484 opposite_vert_tip_idx = None
485 for i in range(0, len(all_chains_tips_idx)):
486 if vert_tip_idx == all_chains_tips_idx[i][0]:
487 opposite_vert_tip_idx = all_chains_tips_idx[i][1]
488 if vert_tip_idx == all_chains_tips_idx[i][1]:
489 opposite_vert_tip_idx = all_chains_tips_idx[i][0]
491 return opposite_vert_tip_idx
493 # Simplifies a spline and returns the new points coordinates
494 def simplify_spline(self, spline_coords, segments_num):
495 simplified_spline = []
496 points_between_segments = round(len(spline_coords) / segments_num)
498 simplified_spline.append(spline_coords[0])
499 for i in range(1, segments_num):
500 simplified_spline.append(spline_coords[i * points_between_segments])
502 simplified_spline.append(spline_coords[len(spline_coords) - 1])
504 return simplified_spline
506 # Returns a list with the coords of the points distributed over the splines
507 # passed to this method according to the proportions parameter
508 def distribute_pts(self, surface_splines, proportions):
510 # Calculate the length of each final surface spline
511 surface_splines_lengths = []
512 surface_splines_parsed = []
514 for sp_idx in range(0, len(surface_splines)):
515 # Calculate spline length
516 surface_splines_lengths.append(0)
518 for i in range(0, len(surface_splines[sp_idx].bezier_points)):
519 if i == 0:
520 prev_p = surface_splines[sp_idx].bezier_points[i]
521 else:
522 p = surface_splines[sp_idx].bezier_points[i]
523 edge_length = (prev_p.co - p.co).length
524 surface_splines_lengths[sp_idx] += edge_length
526 prev_p = p
528 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
529 for sp_idx in range(0, len(surface_splines)):
530 surface_splines_parsed.append([])
531 surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
533 prev_p_co = surface_splines[sp_idx].bezier_points[0].co
534 p_idx = 0
536 for prop_idx in range(len(proportions) - 1):
537 target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
538 partial_segment_length = 0
539 finish_while = False
541 while True:
542 # if not it'll pass the p_idx as an index below and crash
543 if p_idx < len(surface_splines[sp_idx].bezier_points):
544 p_co = surface_splines[sp_idx].bezier_points[p_idx].co
545 new_dist = (prev_p_co - p_co).length
547 # The new distance that could have the partial segment if
548 # it is still shorter than the target length
549 potential_segment_length = partial_segment_length + new_dist
551 # If the potential is still shorter, keep adding
552 if potential_segment_length < target_length:
553 partial_segment_length = potential_segment_length
555 p_idx += 1
556 prev_p_co = p_co
558 # If the potential is longer than the target, calculate the target
559 # (a point between the last two points), and assign
560 elif potential_segment_length > target_length:
561 remaining_dist = target_length - partial_segment_length
562 vec = p_co - prev_p_co
563 vec.normalize()
564 intermediate_co = prev_p_co + (vec * remaining_dist)
566 surface_splines_parsed[sp_idx].append(intermediate_co)
568 partial_segment_length += remaining_dist
569 prev_p_co = intermediate_co
571 finish_while = True
573 # If the potential is equal to the target, assign
574 elif potential_segment_length == target_length:
575 surface_splines_parsed[sp_idx].append(p_co)
576 prev_p_co = p_co
578 finish_while = True
580 if finish_while:
581 break
583 # last point of the spline
584 surface_splines_parsed[sp_idx].append(
585 surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
588 return surface_splines_parsed
590 # Counts the number of faces that belong to each edge
591 def edge_face_count(self, ob):
592 ed_keys_count_dict = {}
594 for face in ob.data.polygons:
595 for ed_keys in face.edge_keys:
596 if ed_keys not in ed_keys_count_dict:
597 ed_keys_count_dict[ed_keys] = 1
598 else:
599 ed_keys_count_dict[ed_keys] += 1
601 edge_face_count = []
602 for i in range(len(ob.data.edges)):
603 edge_face_count.append(0)
605 for i in range(len(ob.data.edges)):
606 ed = ob.data.edges[i]
608 v1 = ed.vertices[0]
609 v2 = ed.vertices[1]
611 if (v1, v2) in ed_keys_count_dict:
612 edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
613 elif (v2, v1) in ed_keys_count_dict:
614 edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
616 return edge_face_count
618 # Fills with faces all the selected vertices which form empty triangles or quads
619 def fill_with_faces(self, object):
620 all_selected_verts_count = self.main_object_selected_verts_count
622 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
624 # Calculate average length of selected edges
625 all_selected_verts = []
626 original_sel_edges_count = 0
627 for ed in object.data.edges:
628 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
629 coords = []
630 coords.append(object.data.vertices[ed.vertices[0]].co)
631 coords.append(object.data.vertices[ed.vertices[1]].co)
633 original_sel_edges_count += 1
635 if not ed.vertices[0] in all_selected_verts:
636 all_selected_verts.append(ed.vertices[0])
638 if not ed.vertices[1] in all_selected_verts:
639 all_selected_verts.append(ed.vertices[1])
641 tuple(all_selected_verts)
643 # Check if there is any edge selected. If not, interrupt the script
644 if original_sel_edges_count == 0 and all_selected_verts_count > 0:
645 return 0
647 # Get all edges connected to selected verts
648 all_edges_around_sel_verts = []
649 edges_connected_to_sel_verts = {}
650 verts_connected_to_every_vert = {}
651 for ed_idx in range(len(object.data.edges)):
652 ed = object.data.edges[ed_idx]
653 include_edge = False
655 if ed.vertices[0] in all_selected_verts:
656 if not ed.vertices[0] in edges_connected_to_sel_verts:
657 edges_connected_to_sel_verts[ed.vertices[0]] = []
659 edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
660 include_edge = True
662 if ed.vertices[1] in all_selected_verts:
663 if not ed.vertices[1] in edges_connected_to_sel_verts:
664 edges_connected_to_sel_verts[ed.vertices[1]] = []
666 edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
667 include_edge = True
669 if include_edge is True:
670 all_edges_around_sel_verts.append(ed_idx)
672 # Get all connected verts to each vert
673 if not ed.vertices[0] in verts_connected_to_every_vert:
674 verts_connected_to_every_vert[ed.vertices[0]] = []
676 if not ed.vertices[1] in verts_connected_to_every_vert:
677 verts_connected_to_every_vert[ed.vertices[1]] = []
679 verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
680 verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
682 # Get all verts connected to faces
683 all_verts_part_of_faces = []
684 all_edges_faces_count = []
685 all_edges_faces_count += self.edge_face_count(object)
687 # Get only the selected edges that have faces attached.
688 count_faces_of_edges_around_sel_verts = {}
689 selected_verts_with_faces = []
690 for ed_idx in all_edges_around_sel_verts:
691 count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
693 if all_edges_faces_count[ed_idx] > 0:
694 ed = object.data.edges[ed_idx]
696 if not ed.vertices[0] in selected_verts_with_faces:
697 selected_verts_with_faces.append(ed.vertices[0])
699 if not ed.vertices[1] in selected_verts_with_faces:
700 selected_verts_with_faces.append(ed.vertices[1])
702 all_verts_part_of_faces.append(ed.vertices[0])
703 all_verts_part_of_faces.append(ed.vertices[1])
705 tuple(selected_verts_with_faces)
707 # Discard unneeded verts from calculations
708 participating_verts = []
709 movable_verts = []
710 for v_idx in all_selected_verts:
711 vert_has_edges_with_one_face = False
713 # Check if the actual vert has at least one edge connected to only one face
714 for ed_idx in edges_connected_to_sel_verts[v_idx]:
715 if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
716 vert_has_edges_with_one_face = True
718 # If the vert has two or less edges connected and the vert is not part of any face.
719 # Or the vert is part of any face and at least one of
720 # the connected edges has only one face attached to it.
721 if (len(edges_connected_to_sel_verts[v_idx]) == 2 and
722 v_idx not in all_verts_part_of_faces) or \
723 len(edges_connected_to_sel_verts[v_idx]) == 1 or \
724 (v_idx in all_verts_part_of_faces and
725 vert_has_edges_with_one_face):
727 participating_verts.append(v_idx)
729 if v_idx not in all_verts_part_of_faces:
730 movable_verts.append(v_idx)
732 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
733 for mv_idx in movable_verts:
734 freeze_vert = False
735 mv_connected_verts = verts_connected_to_every_vert[mv_idx]
737 for actual_v_idx in all_selected_verts:
738 count_shared_neighbors = 0
739 checked_verts = []
741 for mv_conn_v_idx in mv_connected_verts:
742 if mv_idx != actual_v_idx:
743 if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and \
744 mv_conn_v_idx not in checked_verts:
745 count_shared_neighbors += 1
746 checked_verts.append(mv_conn_v_idx)
748 if actual_v_idx in mv_connected_verts:
749 freeze_vert = True
750 break
752 if count_shared_neighbors == 2:
753 freeze_vert = True
754 break
756 if freeze_vert:
757 break
759 if freeze_vert:
760 movable_verts.remove(mv_idx)
762 # Calculate merge distance for participating verts
763 shortest_edge_length = None
764 for ed in object.data.edges:
765 if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
766 v1 = object.data.vertices[ed.vertices[0]]
767 v2 = object.data.vertices[ed.vertices[1]]
769 length = (v1.co - v2.co).length
771 if shortest_edge_length is None:
772 shortest_edge_length = length
773 else:
774 if length < shortest_edge_length:
775 shortest_edge_length = length
777 if shortest_edge_length is not None:
778 edges_merge_distance = shortest_edge_length * 0.5
779 else:
780 edges_merge_distance = 0
782 # Get together the verts near enough. They will be merged later
783 remaining_verts = []
784 remaining_verts += participating_verts
785 for v1_idx in participating_verts:
786 if v1_idx in remaining_verts and v1_idx in movable_verts:
787 verts_to_merge = []
788 coords_verts_to_merge = {}
790 verts_to_merge.append(v1_idx)
792 v1_co = object.data.vertices[v1_idx].co
793 coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
795 for v2_idx in remaining_verts:
796 if v1_idx != v2_idx:
797 v2_co = object.data.vertices[v2_idx].co
799 dist = (v1_co - v2_co).length
801 if dist <= edges_merge_distance: # Add the verts which are near enough
802 verts_to_merge.append(v2_idx)
804 coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
806 for vm_idx in verts_to_merge:
807 remaining_verts.remove(vm_idx)
809 if len(verts_to_merge) > 1:
810 # Calculate middle point of the verts to merge.
811 sum_x_co = 0
812 sum_y_co = 0
813 sum_z_co = 0
814 movable_verts_to_merge_count = 0
815 for i in range(len(verts_to_merge)):
816 if verts_to_merge[i] in movable_verts:
817 v_co = object.data.vertices[verts_to_merge[i]].co
819 sum_x_co += v_co[0]
820 sum_y_co += v_co[1]
821 sum_z_co += v_co[2]
823 movable_verts_to_merge_count += 1
825 middle_point_co = [
826 sum_x_co / movable_verts_to_merge_count,
827 sum_y_co / movable_verts_to_merge_count,
828 sum_z_co / movable_verts_to_merge_count
831 # Check if any vert to be merged is not movable
832 shortest_dist = None
833 are_verts_not_movable = False
834 verts_not_movable = []
835 for v_merge_idx in verts_to_merge:
836 if v_merge_idx in participating_verts and v_merge_idx not in movable_verts:
837 are_verts_not_movable = True
838 verts_not_movable.append(v_merge_idx)
840 if are_verts_not_movable:
841 # Get the vert connected to faces, that is nearest to
842 # the middle point of the movable verts
843 shortest_dist = None
844 for vcf_idx in verts_not_movable:
845 dist = abs((object.data.vertices[vcf_idx].co -
846 Vector(middle_point_co)).length)
848 if shortest_dist is None:
849 shortest_dist = dist
850 nearest_vert_idx = vcf_idx
851 else:
852 if dist < shortest_dist:
853 shortest_dist = dist
854 nearest_vert_idx = vcf_idx
856 coords = object.data.vertices[nearest_vert_idx].co
857 target_point_co = [coords[0], coords[1], coords[2]]
858 else:
859 target_point_co = middle_point_co
861 # Move verts to merge to the middle position
862 for v_merge_idx in verts_to_merge:
863 if v_merge_idx in movable_verts: # Only move the verts that are not part of faces
864 object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
865 object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
866 object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
868 # Perform "Remove Doubles" to weld all the disconnected verts
869 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
870 bpy.ops.mesh.remove_doubles(threshold=0.0001)
872 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
874 # Get all the definitive selected edges, after weldding
875 selected_edges = []
876 edges_per_vert = {} # Number of faces of each selected edge
877 for ed in object.data.edges:
878 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
879 selected_edges.append(ed.index)
881 # Save all the edges that belong to each vertex.
882 if not ed.vertices[0] in edges_per_vert:
883 edges_per_vert[ed.vertices[0]] = []
885 if not ed.vertices[1] in edges_per_vert:
886 edges_per_vert[ed.vertices[1]] = []
888 edges_per_vert[ed.vertices[0]].append(ed.index)
889 edges_per_vert[ed.vertices[1]].append(ed.index)
891 # Check if all the edges connected to each vert have two faces attached to them.
892 # To discard them later and make calculations faster
893 a = []
894 a += self.edge_face_count(object)
895 tuple(a)
896 verts_surrounded_by_faces = {}
897 for v_idx in edges_per_vert:
898 edges_with_two_faces_count = 0
900 for ed_idx in edges_per_vert[v_idx]:
901 if a[ed_idx] == 2:
902 edges_with_two_faces_count += 1
904 if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
905 verts_surrounded_by_faces[v_idx] = True
906 else:
907 verts_surrounded_by_faces[v_idx] = False
909 # Get all the selected vertices
910 selected_verts_idx = []
911 for v in object.data.vertices:
912 if v.select:
913 selected_verts_idx.append(v.index)
915 # Get all the faces of the object
916 all_object_faces_verts_idx = []
917 for face in object.data.polygons:
918 face_verts = []
919 face_verts.append(face.vertices[0])
920 face_verts.append(face.vertices[1])
921 face_verts.append(face.vertices[2])
923 if len(face.vertices) == 4:
924 face_verts.append(face.vertices[3])
926 all_object_faces_verts_idx.append(face_verts)
928 # Deselect all vertices
929 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
930 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
931 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
933 # Make a dictionary with the verts related to each vert
934 related_key_verts = {}
935 for ed_idx in selected_edges:
936 ed = object.data.edges[ed_idx]
938 if not verts_surrounded_by_faces[ed.vertices[0]]:
939 if not ed.vertices[0] in related_key_verts:
940 related_key_verts[ed.vertices[0]] = []
942 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
943 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
945 if not verts_surrounded_by_faces[ed.vertices[1]]:
946 if not ed.vertices[1] in related_key_verts:
947 related_key_verts[ed.vertices[1]] = []
949 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
950 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
952 # Get groups of verts forming each face
953 faces_verts_idx = []
954 for v1 in related_key_verts: # verts-1 ....
955 for v2 in related_key_verts: # verts-2
956 if v1 != v2:
957 related_verts_in_common = []
958 v2_in_rel_v1 = False
959 v1_in_rel_v2 = False
960 for rel_v1 in related_key_verts[v1]:
961 # Check if related verts of verts-1 are related verts of verts-2
962 if rel_v1 in related_key_verts[v2]:
963 related_verts_in_common.append(rel_v1)
965 if v2 in related_key_verts[v1]:
966 v2_in_rel_v1 = True
968 if v1 in related_key_verts[v2]:
969 v1_in_rel_v2 = True
971 repeated_face = False
972 # If two verts have two related verts in common, they form a quad
973 if len(related_verts_in_common) == 2:
974 # Check if the face is already saved
975 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
977 for f_verts in all_faces_to_check_idx:
978 repeated_verts = 0
980 if len(f_verts) == 4:
981 if v1 in f_verts:
982 repeated_verts += 1
983 if v2 in f_verts:
984 repeated_verts += 1
985 if related_verts_in_common[0] in f_verts:
986 repeated_verts += 1
987 if related_verts_in_common[1] in f_verts:
988 repeated_verts += 1
990 if repeated_verts == len(f_verts):
991 repeated_face = True
992 break
994 if not repeated_face:
995 faces_verts_idx.append(
996 [v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
999 # If Two verts have one related vert in common and
1000 # they are related to each other, they form a triangle
1001 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1002 # Check if the face is already saved.
1003 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1005 for f_verts in all_faces_to_check_idx:
1006 repeated_verts = 0
1008 if len(f_verts) == 3:
1009 if v1 in f_verts:
1010 repeated_verts += 1
1011 if v2 in f_verts:
1012 repeated_verts += 1
1013 if related_verts_in_common[0] in f_verts:
1014 repeated_verts += 1
1016 if repeated_verts == len(f_verts):
1017 repeated_face = True
1018 break
1020 if not repeated_face:
1021 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1023 # Keep only the faces that don't overlap by ignoring quads
1024 # that overlap with two adjacent triangles
1025 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1026 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1027 for i in range(len(faces_verts_idx)):
1028 for t in range(len(all_faces_to_check_idx)):
1029 if i != t:
1030 verts_in_common = 0
1032 if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
1033 for v_idx in all_faces_to_check_idx[t]:
1034 if v_idx in faces_verts_idx[i]:
1035 verts_in_common += 1
1036 # If it doesn't have all it's vertices repeated in the other face
1037 if verts_in_common == 3:
1038 if i not in faces_to_not_include_idx:
1039 faces_to_not_include_idx.append(i)
1041 # Build faces discarding the ones in faces_to_not_include
1042 me = object.data
1043 bm = bmesh.new()
1044 bm.from_mesh(me)
1046 num_faces_created = 0
1047 for i in range(len(faces_verts_idx)):
1048 if i not in faces_to_not_include_idx:
1049 bm.faces.new([bm.verts[v] for v in faces_verts_idx[i]])
1051 num_faces_created += 1
1053 bm.to_mesh(me)
1054 bm.free()
1056 for v_idx in selected_verts_idx:
1057 self.main_object.data.vertices[v_idx].select = True
1059 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
1060 bpy.ops.mesh.normals_make_consistent(inside=False)
1061 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
1063 self.update()
1065 return num_faces_created
1067 # Crosshatch skinning
1068 def crosshatch_surface_invoke(self, ob_original_splines):
1069 self.is_crosshatch = False
1070 self.crosshatch_merge_distance = 0
1072 objects_to_delete = [] # duplicated strokes to be deleted.
1074 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1075 # (without this the surface verts merging with the main object doesn't work well)
1076 self.modifiers_prev_viewport_state = []
1077 if len(self.main_object.modifiers) > 0:
1078 for m_idx in range(len(self.main_object.modifiers)):
1079 self.modifiers_prev_viewport_state.append(
1080 self.main_object.modifiers[m_idx].show_viewport
1082 self.main_object.modifiers[m_idx].show_viewport = False
1084 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1085 ob_original_splines.select_set(True)
1086 bpy.context.view_layer.objects.active = ob_original_splines
1088 if len(ob_original_splines.data.splines) >= 2:
1089 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1090 ob_splines = bpy.context.object
1091 ob_splines.name = "SURFSKIO_NE_STR"
1093 # Get estimative merge distance (sum up the distances from the first point to
1094 # all other points, then average them and then divide them)
1095 first_point_dist_sum = 0
1096 first_dist = 0
1097 second_dist = 0
1098 coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
1099 for i in range(len(ob_splines.data.splines)):
1100 sp = ob_splines.data.splines[i]
1102 if coords_first_pt != sp.bezier_points[0].co:
1103 first_dist = (coords_first_pt - sp.bezier_points[0].co).length
1105 if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
1106 second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
1108 first_point_dist_sum += first_dist + second_dist
1110 if i == 0:
1111 if first_dist != 0:
1112 shortest_dist = first_dist
1113 elif second_dist != 0:
1114 shortest_dist = second_dist
1116 if shortest_dist > first_dist and first_dist != 0:
1117 shortest_dist = first_dist
1119 if shortest_dist > second_dist and second_dist != 0:
1120 shortest_dist = second_dist
1122 self.crosshatch_merge_distance = shortest_dist / 20
1124 # Recalculation of merge distance
1126 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1128 ob_calc_merge_dist = bpy.context.object
1129 ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
1131 objects_to_delete.append(ob_calc_merge_dist)
1133 # Smooth out strokes a little to improve crosshatch detection
1134 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1135 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
1137 for i in range(4):
1138 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1140 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1141 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1143 # Convert curves into mesh
1144 ob_calc_merge_dist.data.resolution_u = 12
1145 bpy.ops.object.convert(target='MESH', keep_original=False)
1147 # Find "intersection-nodes"
1148 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1149 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1150 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1151 threshold=self.crosshatch_merge_distance)
1152 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1153 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1155 # Remove verts with less than three edges
1156 verts_edges_count = {}
1157 for ed in ob_calc_merge_dist.data.edges:
1158 v = ed.vertices
1160 if v[0] not in verts_edges_count:
1161 verts_edges_count[v[0]] = 0
1163 if v[1] not in verts_edges_count:
1164 verts_edges_count[v[1]] = 0
1166 verts_edges_count[v[0]] += 1
1167 verts_edges_count[v[1]] += 1
1169 nodes_verts_coords = []
1170 for v_idx in verts_edges_count:
1171 v = ob_calc_merge_dist.data.vertices[v_idx]
1173 if verts_edges_count[v_idx] < 3:
1174 v.select = True
1176 # Remove them
1177 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1178 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
1179 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1181 # Remove doubles to discard very near verts from calculations of distance
1182 bpy.ops.mesh.remove_doubles(
1183 'INVOKE_REGION_WIN',
1184 threshold=self.crosshatch_merge_distance * 4.0
1186 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1188 # Get all coords of the resulting nodes
1189 nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for
1190 v in ob_calc_merge_dist.data.vertices]
1192 # Check if the strokes are a crosshatch
1193 if len(nodes_verts_coords) >= 3:
1194 self.is_crosshatch = True
1196 shortest_dist = None
1197 for co_1 in nodes_verts_coords:
1198 for co_2 in nodes_verts_coords:
1199 if co_1 != co_2:
1200 dist = (Vector(co_1) - Vector(co_2)).length
1202 if shortest_dist is not None:
1203 if dist < shortest_dist:
1204 shortest_dist = dist
1205 else:
1206 shortest_dist = dist
1208 self.crosshatch_merge_distance = shortest_dist / 3
1210 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1211 ob_splines.select_set(True)
1212 bpy.context.view_layer.objects.active = ob_splines
1214 # Deselect all points
1215 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1216 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1217 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1219 # Smooth splines in a localized way, to eliminate "saw-teeth"
1220 # like shapes when there are many points
1221 for sp in ob_splines.data.splines:
1222 angle_sum = 0
1224 angle_limit = 2 # Degrees
1225 for t in range(len(sp.bezier_points)):
1226 # Because on each iteration it checks the "next two points"
1227 # of the actual. This way it doesn't go out of range
1228 if t <= len(sp.bezier_points) - 3:
1229 p1 = sp.bezier_points[t]
1230 p2 = sp.bezier_points[t + 1]
1231 p3 = sp.bezier_points[t + 2]
1233 vec_1 = p1.co - p2.co
1234 vec_2 = p2.co - p3.co
1236 if p2.co != p1.co and p2.co != p3.co:
1237 angle = vec_1.angle(vec_2)
1238 angle_sum += degrees(angle)
1240 if angle_sum >= angle_limit: # If sum of angles is grater than the limit
1241 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1242 p1.select_control_point = True
1243 p1.select_left_handle = True
1244 p1.select_right_handle = True
1246 p2.select_control_point = True
1247 p2.select_left_handle = True
1248 p2.select_right_handle = True
1250 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1251 p3.select_control_point = True
1252 p3.select_left_handle = True
1253 p3.select_right_handle = True
1255 angle_sum = 0
1257 sp.bezier_points[0].select_control_point = False
1258 sp.bezier_points[0].select_left_handle = False
1259 sp.bezier_points[0].select_right_handle = False
1261 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
1262 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
1263 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
1265 # Smooth out strokes a little to improve crosshatch detection
1266 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1268 for i in range(15):
1269 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1271 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1272 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1274 # Simplify the splines
1275 for sp in ob_splines.data.splines:
1276 angle_sum = 0
1278 sp.bezier_points[0].select_control_point = True
1279 sp.bezier_points[0].select_left_handle = True
1280 sp.bezier_points[0].select_right_handle = True
1282 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
1283 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
1284 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
1286 angle_limit = 15 # Degrees
1287 for t in range(len(sp.bezier_points)):
1288 # Because on each iteration it checks the "next two points"
1289 # of the actual. This way it doesn't go out of range
1290 if t <= len(sp.bezier_points) - 3:
1291 p1 = sp.bezier_points[t]
1292 p2 = sp.bezier_points[t + 1]
1293 p3 = sp.bezier_points[t + 2]
1295 vec_1 = p1.co - p2.co
1296 vec_2 = p2.co - p3.co
1298 if p2.co != p1.co and p2.co != p3.co:
1299 angle = vec_1.angle(vec_2)
1300 angle_sum += degrees(angle)
1301 # If sum of angles is grater than the limit
1302 if angle_sum >= angle_limit:
1303 p1.select_control_point = True
1304 p1.select_left_handle = True
1305 p1.select_right_handle = True
1307 p2.select_control_point = True
1308 p2.select_left_handle = True
1309 p2.select_right_handle = True
1311 p3.select_control_point = True
1312 p3.select_left_handle = True
1313 p3.select_right_handle = True
1315 angle_sum = 0
1317 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1318 bpy.ops.curve.select_all(action='INVERT')
1320 bpy.ops.curve.delete(type='VERT')
1321 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1323 objects_to_delete.append(ob_splines)
1325 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1326 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1327 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1329 # Check if the strokes are a crosshatch
1330 if self.is_crosshatch:
1331 all_points_coords = []
1332 for i in range(len(ob_splines.data.splines)):
1333 all_points_coords.append([])
1335 all_points_coords[i] = [Vector((x, y, z)) for
1336 x, y, z in [bp.co for
1337 bp in ob_splines.data.splines[i].bezier_points]]
1339 all_intersections = []
1340 checked_splines = []
1341 for i in range(len(all_points_coords)):
1343 for t in range(len(all_points_coords[i]) - 1):
1344 bp1_co = all_points_coords[i][t]
1345 bp2_co = all_points_coords[i][t + 1]
1347 for i2 in range(len(all_points_coords)):
1348 if i != i2 and i2 not in checked_splines:
1349 for t2 in range(len(all_points_coords[i2]) - 1):
1350 bp3_co = all_points_coords[i2][t2]
1351 bp4_co = all_points_coords[i2][t2 + 1]
1353 intersec_coords = intersect_line_line(
1354 bp1_co, bp2_co, bp3_co, bp4_co
1356 if intersec_coords is not None:
1357 dist = (intersec_coords[0] - intersec_coords[1]).length
1359 if dist <= self.crosshatch_merge_distance * 1.5:
1360 _temp_co, percent1 = intersect_point_line(
1361 intersec_coords[0], bp1_co, bp2_co
1363 if (percent1 >= -0.02 and percent1 <= 1.02):
1364 _temp_co, percent2 = intersect_point_line(
1365 intersec_coords[1], bp3_co, bp4_co
1367 if (percent2 >= -0.02 and percent2 <= 1.02):
1368 # Format: spline index, first point index from
1369 # corresponding segment, percentage from first point of
1370 # actual segment, coords of intersection point
1371 all_intersections.append(
1372 (i, t, percent1,
1373 ob_splines.matrix_world @ intersec_coords[0])
1375 all_intersections.append(
1376 (i2, t2, percent2,
1377 ob_splines.matrix_world @ intersec_coords[1])
1380 checked_splines.append(i)
1381 # Sort list by spline, then by corresponding first point index of segment,
1382 # and then by percentage from first point of segment: elements 0 and 1 respectively
1383 all_intersections.sort(key=operator.itemgetter(0, 1, 2))
1385 self.crosshatch_strokes_coords = {}
1386 for i in range(len(all_intersections)):
1387 if not all_intersections[i][0] in self.crosshatch_strokes_coords:
1388 self.crosshatch_strokes_coords[all_intersections[i][0]] = []
1390 self.crosshatch_strokes_coords[all_intersections[i][0]].append(
1391 all_intersections[i][3]
1392 ) # Save intersection coords
1393 else:
1394 self.is_crosshatch = False
1396 # Delete all duplicates
1397 bpy.ops.object.delete({"selected_objects": objects_to_delete})
1399 # If the main object has modifiers, turn their "viewport view status" to
1400 # what it was before the forced deactivation above
1401 if len(self.main_object.modifiers) > 0:
1402 for m_idx in range(len(self.main_object.modifiers)):
1403 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1405 self.update()
1407 return
1409 # Part of the Crosshatch process that is repeated when the operator is tweaked
1410 def crosshatch_surface_execute(self, context):
1411 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1412 # (without this the surface verts merging with the main object doesn't work well)
1413 self.modifiers_prev_viewport_state = []
1414 if len(self.main_object.modifiers) > 0:
1415 for m_idx in range(len(self.main_object.modifiers)):
1416 self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
1418 self.main_object.modifiers[m_idx].show_viewport = False
1420 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1422 me_name = "SURFSKIO_STK_TMP"
1423 me = bpy.data.meshes.new(me_name)
1425 all_verts_coords = []
1426 all_edges = []
1427 for st_idx in self.crosshatch_strokes_coords:
1428 for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
1429 coords = self.crosshatch_strokes_coords[st_idx][co_idx]
1431 all_verts_coords.append(coords)
1433 if co_idx > 0:
1434 all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
1436 me.from_pydata(all_verts_coords, all_edges, [])
1437 ob = object_utils.object_data_add(context, me)
1439 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1440 ob.select_set(True)
1441 bpy.context.view_layer.objects.active = ob
1443 # Get together each vert and its nearest, to the middle position
1444 verts = ob.data.vertices
1445 checked_verts = []
1446 for i in range(len(verts)):
1447 shortest_dist = None
1449 if i not in checked_verts:
1450 for t in range(len(verts)):
1451 if i != t and t not in checked_verts:
1452 dist = (verts[i].co - verts[t].co).length
1454 if shortest_dist is not None:
1455 if dist < shortest_dist:
1456 shortest_dist = dist
1457 nearest_vert = t
1458 else:
1459 shortest_dist = dist
1460 nearest_vert = t
1462 middle_location = (verts[i].co + verts[nearest_vert].co) / 2
1464 verts[i].co = middle_location
1465 verts[nearest_vert].co = middle_location
1467 checked_verts.append(i)
1468 checked_verts.append(nearest_vert)
1470 # Calculate average length between all the generated edges
1471 ob = bpy.context.object
1472 lengths_sum = 0
1473 for ed in ob.data.edges:
1474 v1 = ob.data.vertices[ed.vertices[0]]
1475 v2 = ob.data.vertices[ed.vertices[1]]
1477 lengths_sum += (v1.co - v2.co).length
1479 edges_count = len(ob.data.edges)
1480 # possible division by zero here
1481 average_edge_length = lengths_sum / edges_count if edges_count != 0 else 0.0001
1483 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1484 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1485 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1486 threshold=average_edge_length / 15.0)
1487 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1489 final_points_ob = bpy.context.view_layer.objects.active
1491 # Make a dictionary with the verts related to each vert
1492 related_key_verts = {}
1493 for ed in final_points_ob.data.edges:
1494 if not ed.vertices[0] in related_key_verts:
1495 related_key_verts[ed.vertices[0]] = []
1497 if not ed.vertices[1] in related_key_verts:
1498 related_key_verts[ed.vertices[1]] = []
1500 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
1501 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
1503 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
1504 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
1506 # Get groups of verts forming each face
1507 faces_verts_idx = []
1508 for v1 in related_key_verts: # verts-1 ....
1509 for v2 in related_key_verts: # verts-2
1510 if v1 != v2:
1511 related_verts_in_common = []
1512 v2_in_rel_v1 = False
1513 v1_in_rel_v2 = False
1514 for rel_v1 in related_key_verts[v1]:
1515 # Check if related verts of verts-1 are related verts of verts-2
1516 if rel_v1 in related_key_verts[v2]:
1517 related_verts_in_common.append(rel_v1)
1519 if v2 in related_key_verts[v1]:
1520 v2_in_rel_v1 = True
1522 if v1 in related_key_verts[v2]:
1523 v1_in_rel_v2 = True
1525 repeated_face = False
1526 # If two verts have two related verts in common, they form a quad
1527 if len(related_verts_in_common) == 2:
1528 # Check if the face is already saved
1529 for f_verts in faces_verts_idx:
1530 repeated_verts = 0
1532 if len(f_verts) == 4:
1533 if v1 in f_verts:
1534 repeated_verts += 1
1535 if v2 in f_verts:
1536 repeated_verts += 1
1537 if related_verts_in_common[0] in f_verts:
1538 repeated_verts += 1
1539 if related_verts_in_common[1] in f_verts:
1540 repeated_verts += 1
1542 if repeated_verts == len(f_verts):
1543 repeated_face = True
1544 break
1546 if not repeated_face:
1547 faces_verts_idx.append([v1, related_verts_in_common[0],
1548 v2, related_verts_in_common[1]])
1550 # If Two verts have one related vert in common and they are
1551 # related to each other, they form a triangle
1552 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1553 # Check if the face is already saved.
1554 for f_verts in faces_verts_idx:
1555 repeated_verts = 0
1557 if len(f_verts) == 3:
1558 if v1 in f_verts:
1559 repeated_verts += 1
1560 if v2 in f_verts:
1561 repeated_verts += 1
1562 if related_verts_in_common[0] in f_verts:
1563 repeated_verts += 1
1565 if repeated_verts == len(f_verts):
1566 repeated_face = True
1567 break
1569 if not repeated_face:
1570 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1572 # Keep only the faces that don't overlap by ignoring
1573 # quads that overlap with two adjacent triangles
1574 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1575 for i in range(len(faces_verts_idx)):
1576 for t in range(len(faces_verts_idx)):
1577 if i != t:
1578 verts_in_common = 0
1580 if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
1581 for v_idx in faces_verts_idx[t]:
1582 if v_idx in faces_verts_idx[i]:
1583 verts_in_common += 1
1584 # If it doesn't have all it's vertices repeated in the other face
1585 if verts_in_common == 3:
1586 if i not in faces_to_not_include_idx:
1587 faces_to_not_include_idx.append(i)
1589 # Build surface
1590 all_surface_verts_co = []
1591 for i in range(len(final_points_ob.data.vertices)):
1592 coords = final_points_ob.data.vertices[i].co
1593 all_surface_verts_co.append([coords[0], coords[1], coords[2]])
1595 # Verts of each face.
1596 all_surface_faces = []
1597 for i in range(len(faces_verts_idx)):
1598 if i not in faces_to_not_include_idx:
1599 face = []
1600 for v_idx in faces_verts_idx[i]:
1601 face.append(v_idx)
1603 all_surface_faces.append(face)
1605 # Build the mesh
1606 surf_me_name = "SURFSKIO_surface"
1607 me_surf = bpy.data.meshes.new(surf_me_name)
1608 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
1609 ob_surface = object_utils.object_data_add(context, me_surf)
1611 # Delete final points temporal object
1612 bpy.ops.object.delete({"selected_objects": [final_points_ob]})
1614 # Delete isolated verts if there are any
1615 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1616 ob_surface.select_set(True)
1617 bpy.context.view_layer.objects.active = ob_surface
1619 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1620 bpy.ops.mesh.select_all(action='DESELECT')
1621 bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
1622 bpy.ops.mesh.delete()
1623 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1625 # Join crosshatch results with original mesh
1627 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1628 edges_length_sum = 0
1629 for ed in ob_surface.data.edges:
1630 edges_length_sum += (
1631 ob_surface.data.vertices[ed.vertices[0]].co -
1632 ob_surface.data.vertices[ed.vertices[1]].co
1633 ).length
1635 # Make dictionary with all the verts connected to each vert, on the new surface object.
1636 surface_connected_verts = {}
1637 for ed in ob_surface.data.edges:
1638 if not ed.vertices[0] in surface_connected_verts:
1639 surface_connected_verts[ed.vertices[0]] = []
1641 surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
1643 if ed.vertices[1] not in surface_connected_verts:
1644 surface_connected_verts[ed.vertices[1]] = []
1646 surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
1648 # Duplicate the new surface object, and use shrinkwrap to
1649 # calculate later the nearest verts to the main object
1650 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1651 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1652 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1654 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1656 final_ob_duplicate = bpy.context.view_layer.objects.active
1658 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1659 shrinkwrap_modifier = final_ob_duplicate.modifiers[-1]
1660 shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
1661 shrinkwrap_modifier.target = self.main_object
1663 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier=shrinkwrap_modifier.name)
1665 # Make list with verts of original mesh as index and coords as value
1666 main_object_verts_coords = []
1667 for v in self.main_object.data.vertices:
1668 coords = self.main_object.matrix_world @ v.co
1670 # To avoid problems when taking "-0.00" as a different value as "0.00"
1671 for c in range(len(coords)):
1672 if "%.3f" % coords[c] == "-0.00":
1673 coords[c] = 0
1675 main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
1677 tuple(main_object_verts_coords)
1679 # Determine which verts will be merged, snap them to the nearest verts
1680 # on the original verts, and get them selected
1681 crosshatch_verts_to_merge = []
1682 if self.automatic_join:
1683 for i in range(len(ob_surface.data.vertices)-1):
1684 # Calculate the distance from each of the connected verts to the actual vert,
1685 # and compare it with the distance they would have if joined.
1686 # If they don't change much, that vert can be joined
1687 merge_actual_vert = True
1688 try:
1689 if len(surface_connected_verts[i]) < 4:
1690 for c_v_idx in surface_connected_verts[i]:
1691 points_original = []
1692 points_original.append(ob_surface.data.vertices[c_v_idx].co)
1693 points_original.append(ob_surface.data.vertices[i].co)
1695 points_target = []
1696 points_target.append(ob_surface.data.vertices[c_v_idx].co)
1697 points_target.append(final_ob_duplicate.data.vertices[i].co)
1699 vec_A = points_original[0] - points_original[1]
1700 vec_B = points_target[0] - points_target[1]
1702 dist_A = (points_original[0] - points_original[1]).length
1703 dist_B = (points_target[0] - points_target[1]).length
1705 if not (
1706 points_original[0] == points_original[1] or
1707 points_target[0] == points_target[1]
1708 ): # If any vector's length is zero
1710 angle = vec_A.angle(vec_B) / pi
1711 else:
1712 angle = 0
1714 # Set a range of acceptable variation in the connected edges
1715 if dist_B > dist_A * 1.7 * self.join_stretch_factor or \
1716 dist_B < dist_A / 2 / self.join_stretch_factor or \
1717 angle >= 0.15 * self.join_stretch_factor:
1719 merge_actual_vert = False
1720 break
1721 else:
1722 merge_actual_vert = False
1723 except:
1724 self.report({'WARNING'},
1725 "Crosshatch set incorrectly")
1727 if merge_actual_vert:
1728 coords = final_ob_duplicate.data.vertices[i].co
1729 # To avoid problems when taking "-0.000" as a different value as "0.00"
1730 for c in range(len(coords)):
1731 if "%.3f" % coords[c] == "-0.00":
1732 coords[c] = 0
1734 comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
1736 if comparison_coords in main_object_verts_coords:
1737 # Get the index of the vert with those coords in the main object
1738 main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords)
1740 if self.main_object.data.vertices[main_object_related_vert_idx].select is True or \
1741 self.main_object_selected_verts_count == 0:
1743 ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
1744 ob_surface.data.vertices[i].select = True
1745 crosshatch_verts_to_merge.append(i)
1747 # Make sure the vert in the main object is selected,
1748 # in case it wasn't selected and the "join crosshatch" option is active
1749 self.main_object.data.vertices[main_object_related_vert_idx].select = True
1751 # Delete duplicated object
1752 bpy.ops.object.delete({"selected_objects": [final_ob_duplicate]})
1754 # Join crosshatched surface and main object
1755 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1756 ob_surface.select_set(True)
1757 self.main_object.select_set(True)
1758 bpy.context.view_layer.objects.active = self.main_object
1760 bpy.ops.object.join('INVOKE_REGION_WIN')
1762 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1763 # Perform Remove doubles to merge verts
1764 if not (self.automatic_join is False and self.main_object_selected_verts_count == 0):
1765 bpy.ops.mesh.remove_doubles(threshold=0.0001)
1767 bpy.ops.mesh.select_all(action='DESELECT')
1769 # If the main object has modifiers, turn their "viewport view status"
1770 # to what it was before the forced deactivation above
1771 if len(self.main_object.modifiers) > 0:
1772 for m_idx in range(len(self.main_object.modifiers)):
1773 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1775 self.update()
1777 return {'FINISHED'}
1779 def rectangular_surface(self, context):
1780 # Selected edges
1781 all_selected_edges_idx = []
1782 all_selected_verts = []
1783 all_verts_idx = []
1784 for ed in self.main_object.data.edges:
1785 if ed.select:
1786 all_selected_edges_idx.append(ed.index)
1788 # Selected vertices
1789 if not ed.vertices[0] in all_selected_verts:
1790 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
1791 if not ed.vertices[1] in all_selected_verts:
1792 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
1794 # All verts (both from each edge) to determine later
1795 # which are at the tips (those not repeated twice)
1796 all_verts_idx.append(ed.vertices[0])
1797 all_verts_idx.append(ed.vertices[1])
1799 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1800 all_chains_tips_idx = []
1801 for v_idx in all_verts_idx:
1802 if all_verts_idx.count(v_idx) < 2:
1803 all_chains_tips_idx.append(v_idx)
1805 edges_connected_to_tips = []
1806 for ed in self.main_object.data.edges:
1807 if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and \
1808 not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
1810 edges_connected_to_tips.append(ed)
1812 # Check closed selections
1813 # List with groups of three verts, where the first element of the pair is
1814 # the unselected vert of a closed selection and the other two elements are the
1815 # selected neighbor verts (it will be useful to determine which selection chain
1816 # the unselected vert belongs to, and determine the "middle-vertex")
1817 single_unselected_verts_and_neighbors = []
1819 # To identify a "closed" selection (a selection that is a closed chain except
1820 # for one vertex) find the vertex in common that have the edges connected to tips.
1821 # If there is a vertex in common, that one is the unselected vert that closes
1822 # the selection or is a "middle-vertex"
1823 single_unselected_verts = []
1824 for ed in edges_connected_to_tips:
1825 for ed_b in edges_connected_to_tips:
1826 if ed != ed_b:
1827 if ed.vertices[0] == ed_b.vertices[0] and \
1828 not self.main_object.data.vertices[ed.vertices[0]].select and \
1829 ed.vertices[0] not in single_unselected_verts:
1831 # The second element is one of the tips of the selected
1832 # vertices of the closed selection
1833 single_unselected_verts_and_neighbors.append(
1834 [ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]
1836 single_unselected_verts.append(ed.vertices[0])
1837 break
1838 elif ed.vertices[0] == ed_b.vertices[1] and \
1839 not self.main_object.data.vertices[ed.vertices[0]].select and \
1840 ed.vertices[0] not in single_unselected_verts:
1842 single_unselected_verts_and_neighbors.append(
1843 [ed.vertices[0], ed.vertices[1], ed_b.vertices[0]]
1845 single_unselected_verts.append(ed.vertices[0])
1846 break
1847 elif ed.vertices[1] == ed_b.vertices[0] and \
1848 not self.main_object.data.vertices[ed.vertices[1]].select and \
1849 ed.vertices[1] not in single_unselected_verts:
1851 single_unselected_verts_and_neighbors.append(
1852 [ed.vertices[1], ed.vertices[0], ed_b.vertices[1]]
1854 single_unselected_verts.append(ed.vertices[1])
1855 break
1856 elif ed.vertices[1] == ed_b.vertices[1] and \
1857 not self.main_object.data.vertices[ed.vertices[1]].select and \
1858 ed.vertices[1] not in single_unselected_verts:
1860 single_unselected_verts_and_neighbors.append(
1861 [ed.vertices[1], ed.vertices[0], ed_b.vertices[0]]
1863 single_unselected_verts.append(ed.vertices[1])
1864 break
1866 middle_vertex_idx = None
1867 tips_to_discard_idx = []
1869 # Check if there is a "middle-vertex", and get its index
1870 for i in range(0, len(single_unselected_verts_and_neighbors)):
1871 actual_chain_verts = self.get_ordered_verts(
1872 self.main_object, all_selected_edges_idx,
1873 all_verts_idx, single_unselected_verts_and_neighbors[i][1],
1874 None, None
1877 if single_unselected_verts_and_neighbors[i][2] != \
1878 actual_chain_verts[len(actual_chain_verts) - 1].index:
1880 middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
1881 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
1882 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
1884 # List with pairs of verts that belong to the tips of each selection chain (row)
1885 verts_tips_same_chain_idx = []
1886 if len(all_chains_tips_idx) >= 2:
1887 checked_v = []
1888 for i in range(0, len(all_chains_tips_idx)):
1889 if all_chains_tips_idx[i] not in checked_v:
1890 v_chain = self.get_ordered_verts(
1891 self.main_object, all_selected_edges_idx,
1892 all_verts_idx, all_chains_tips_idx[i],
1893 middle_vertex_idx, None
1896 verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
1898 checked_v.append(v_chain[0].index)
1899 checked_v.append(v_chain[len(v_chain) - 1].index)
1901 # Selection tips (vertices).
1902 verts_tips_parsed_idx = []
1903 if len(all_chains_tips_idx) >= 2:
1904 for spec_v_idx in all_chains_tips_idx:
1905 if (spec_v_idx not in tips_to_discard_idx):
1906 verts_tips_parsed_idx.append(spec_v_idx)
1908 # Identify the type of selection made by the user
1909 if middle_vertex_idx is not None:
1910 # If there are 4 tips (two selection chains), and
1911 # there is only one single unselected vert (the middle vert)
1912 if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1:
1913 selection_type = "TWO_CONNECTED"
1914 else:
1915 # The type of the selection was not identified, the script stops.
1916 self.report({'WARNING'}, "The selection isn't valid.")
1918 self.stopping_errors = True
1920 return{'CANCELLED'}
1921 else:
1922 if len(all_chains_tips_idx) == 2: # If there are 2 tips
1923 selection_type = "SINGLE"
1924 elif len(all_chains_tips_idx) == 4: # If there are 4 tips
1925 selection_type = "TWO_NOT_CONNECTED"
1926 elif len(all_chains_tips_idx) == 0:
1927 if len(self.main_splines.data.splines) > 1:
1928 selection_type = "NO_SELECTION"
1929 else:
1930 # If the selection was not identified and there is only one stroke,
1931 # there's no possibility to build a surface, so the script is interrupted
1932 self.report({'WARNING'}, "The selection isn't valid.")
1934 self.stopping_errors = True
1936 return{'CANCELLED'}
1937 else:
1938 # The type of the selection was not identified, the script stops
1939 self.report({'WARNING'}, "The selection isn't valid.")
1941 self.stopping_errors = True
1943 return{'CANCELLED'}
1945 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1946 if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
1947 self.report({'WARNING'},
1948 "At least two strokes are needed when there are two not connected selections")
1950 self.stopping_errors = True
1952 return{'CANCELLED'}
1954 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1956 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1957 self.main_splines.select_set(True)
1958 bpy.context.view_layer.objects.active = self.main_splines
1960 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1961 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1962 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1963 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1964 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1965 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1966 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1967 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1968 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1970 self.selection_U_exists = False
1971 self.selection_U2_exists = False
1972 self.selection_V_exists = False
1973 self.selection_V2_exists = False
1975 self.selection_U_is_closed = False
1976 self.selection_U2_is_closed = False
1977 self.selection_V_is_closed = False
1978 self.selection_V2_is_closed = False
1980 # Define what vertices are at the tips of each selection and are not the middle-vertex
1981 if selection_type == "TWO_CONNECTED":
1982 self.selection_U_exists = True
1983 self.selection_V_exists = True
1985 closing_vert_U_idx = None
1986 closing_vert_V_idx = None
1987 closing_vert_U2_idx = None
1988 closing_vert_V2_idx = None
1990 # Determine which selection is Selection-U and which is Selection-V
1991 points_A = []
1992 points_B = []
1993 points_first_stroke_tips = []
1995 points_A.append(
1996 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[0]].co
1998 points_A.append(
1999 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
2001 points_B.append(
2002 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[1]].co
2004 points_B.append(
2005 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
2007 points_first_stroke_tips.append(
2008 self.main_splines.data.splines[0].bezier_points[0].co
2010 points_first_stroke_tips.append(
2011 self.main_splines.data.splines[0].bezier_points[
2012 len(self.main_splines.data.splines[0].bezier_points) - 1
2013 ].co
2016 angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
2017 angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
2019 if angle_A < angle_B:
2020 first_vert_U_idx = verts_tips_parsed_idx[0]
2021 first_vert_V_idx = verts_tips_parsed_idx[1]
2022 else:
2023 first_vert_U_idx = verts_tips_parsed_idx[1]
2024 first_vert_V_idx = verts_tips_parsed_idx[0]
2026 elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
2027 first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
2028 last_sketched_point_first_stroke_co = \
2029 self.main_splines.data.splines[0].bezier_points[
2030 len(self.main_splines.data.splines[0].bezier_points) - 1
2031 ].co
2032 first_sketched_point_last_stroke_co = \
2033 self.main_splines.data.splines[
2034 len(self.main_splines.data.splines) - 1
2035 ].bezier_points[0].co
2036 if len(self.main_splines.data.splines) > 1:
2037 first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
2038 last_sketched_point_second_stroke_co = \
2039 self.main_splines.data.splines[1].bezier_points[
2040 len(self.main_splines.data.splines[1].bezier_points) - 1
2041 ].co
2043 single_unselected_neighbors = [] # Only the neighbors of the single unselected verts
2044 for verts_neig_idx in single_unselected_verts_and_neighbors:
2045 single_unselected_neighbors.append(verts_neig_idx[1])
2046 single_unselected_neighbors.append(verts_neig_idx[2])
2048 all_chains_tips_and_middle_vert = []
2049 for v_idx in all_chains_tips_idx:
2050 if v_idx not in single_unselected_neighbors:
2051 all_chains_tips_and_middle_vert.append(v_idx)
2053 all_chains_tips_and_middle_vert += single_unselected_verts
2055 all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
2057 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2058 nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = \
2059 self.shortest_distance(
2060 self.main_object,
2061 first_sketched_point_first_stroke_co,
2062 all_chains_tips_and_middle_vert
2064 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2065 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2066 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2068 nearest_tip_to_first_st_first_pt_opposite_idx = \
2069 self.opposite_tip(
2070 nearest_tip_to_first_st_first_pt_idx,
2071 verts_tips_same_chain_idx
2073 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2074 nearest_tip_to_first_st_last_pt_idx, _temp_dist = \
2075 self.shortest_distance(
2076 self.main_object,
2077 last_sketched_point_first_stroke_co,
2078 all_chains_tips_and_middle_vert
2080 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2081 nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = \
2082 self.shortest_distance(
2083 self.main_object,
2084 first_sketched_point_last_stroke_co,
2085 all_chains_tips_and_middle_vert
2087 if len(self.main_splines.data.splines) > 1:
2088 # The selected vertex nearest to the first point of the second sketched stroke
2089 # (This will be useful to determine the direction of the closed
2090 # selection V when extruding along strokes)
2091 nearest_vert_to_second_st_first_pt_idx, _temp_dist = \
2092 self.shortest_distance(
2093 self.main_object,
2094 first_sketched_point_second_stroke_co,
2095 all_verts_idx
2097 # The selected vertex nearest to the first point of the second sketched stroke
2098 # (This will be useful to determine the direction of the closed
2099 # selection V2 when extruding along strokes)
2100 nearest_vert_to_second_st_last_pt_idx, _temp_dist = \
2101 self.shortest_distance(
2102 self.main_object,
2103 last_sketched_point_second_stroke_co,
2104 all_verts_idx
2106 # Determine if the single selection will be treated as U or as V
2107 edges_sum = 0
2108 for i in all_selected_edges_idx:
2109 edges_sum += (
2110 (self.main_object.matrix_world @
2111 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) -
2112 (self.main_object.matrix_world @
2113 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)
2114 ).length
2116 average_edge_length = edges_sum / len(all_selected_edges_idx)
2118 # Get shortest distance from the first point of the last stroke to any participating vertex
2119 _temp_idx, shortest_distance_to_last_stroke = \
2120 self.shortest_distance(
2121 self.main_object,
2122 first_sketched_point_last_stroke_co,
2123 all_participating_verts
2125 # If the beginning of the first stroke is near enough, and its orientation
2126 # difference with the first edge of the nearest selection chain is not too high,
2127 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2128 if shortest_distance_to_first_stroke < average_edge_length / 4 and \
2129 shortest_distance_to_last_stroke < average_edge_length and \
2130 len(self.main_splines.data.splines) > 1:
2132 self.selection_U_exists = False
2133 self.selection_V_exists = True
2134 # If the first selection is not closed
2135 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2136 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2137 self.selection_V_is_closed = False
2138 closing_vert_U_idx = None
2139 closing_vert_U2_idx = None
2140 closing_vert_V_idx = None
2141 closing_vert_V2_idx = None
2143 first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2145 if selection_type == "TWO_NOT_CONNECTED":
2146 self.selection_V2_exists = True
2148 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2149 else:
2150 self.selection_V_is_closed = True
2151 closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2153 # Get the neighbors of the first (unselected) vert of the closed selection U.
2154 vert_neighbors = []
2155 for verts in single_unselected_verts_and_neighbors:
2156 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2157 vert_neighbors.append(verts[1])
2158 vert_neighbors.append(verts[2])
2159 break
2161 verts_V = self.get_ordered_verts(
2162 self.main_object, all_selected_edges_idx,
2163 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2166 for i in range(0, len(verts_V)):
2167 if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
2168 # If the vertex nearest to the first point of the second stroke
2169 # is in the first half of the selected verts
2170 if i >= len(verts_V) / 2:
2171 first_vert_V_idx = vert_neighbors[1]
2172 break
2173 else:
2174 first_vert_V_idx = vert_neighbors[0]
2175 break
2177 if selection_type == "TWO_NOT_CONNECTED":
2178 self.selection_V2_exists = True
2179 # If the second selection is not closed
2180 if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or \
2181 nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx:
2183 self.selection_V2_is_closed = False
2184 closing_vert_V2_idx = None
2185 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2187 else:
2188 self.selection_V2_is_closed = True
2189 closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2191 # Get the neighbors of the first (unselected) vert of the closed selection U
2192 vert_neighbors = []
2193 for verts in single_unselected_verts_and_neighbors:
2194 if verts[0] == nearest_tip_to_first_st_last_pt_idx:
2195 vert_neighbors.append(verts[1])
2196 vert_neighbors.append(verts[2])
2197 break
2199 verts_V2 = self.get_ordered_verts(
2200 self.main_object, all_selected_edges_idx,
2201 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2204 for i in range(0, len(verts_V2)):
2205 if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
2206 # If the vertex nearest to the first point of the second stroke
2207 # is in the first half of the selected verts
2208 if i >= len(verts_V2) / 2:
2209 first_vert_V2_idx = vert_neighbors[1]
2210 break
2211 else:
2212 first_vert_V2_idx = vert_neighbors[0]
2213 break
2214 else:
2215 self.selection_V2_exists = False
2217 else:
2218 self.selection_U_exists = True
2219 self.selection_V_exists = False
2220 # If the first selection is not closed
2221 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2222 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2223 self.selection_U_is_closed = False
2224 closing_vert_U_idx = None
2226 points_tips = []
2227 points_tips.append(
2228 self.main_object.matrix_world @
2229 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2231 points_tips.append(
2232 self.main_object.matrix_world @
2233 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co
2235 points_first_stroke_tips = []
2236 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2237 points_first_stroke_tips.append(
2238 self.main_splines.data.splines[0].bezier_points[
2239 len(self.main_splines.data.splines[0].bezier_points) - 1
2240 ].co
2242 vec_A = points_tips[0] - points_tips[1]
2243 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2245 # Compare the direction of the selection and the first
2246 # grease pencil stroke to determine which is the "first" vertex of the selection
2247 if vec_A.dot(vec_B) < 0:
2248 first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
2249 else:
2250 first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2252 else:
2253 self.selection_U_is_closed = True
2254 closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2256 # Get the neighbors of the first (unselected) vert of the closed selection U
2257 vert_neighbors = []
2258 for verts in single_unselected_verts_and_neighbors:
2259 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2260 vert_neighbors.append(verts[1])
2261 vert_neighbors.append(verts[2])
2262 break
2264 points_first_and_neighbor = []
2265 points_first_and_neighbor.append(
2266 self.main_object.matrix_world @
2267 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2269 points_first_and_neighbor.append(
2270 self.main_object.matrix_world @
2271 self.main_object.data.vertices[vert_neighbors[0]].co
2273 points_first_stroke_tips = []
2274 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2275 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
2277 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2278 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2280 # Compare the direction of the selection and the first grease pencil stroke to
2281 # determine which is the vertex neighbor to the first vertex (unselected) of
2282 # the closed selection. This will determine the direction of the closed selection
2283 if vec_A.dot(vec_B) < 0:
2284 first_vert_U_idx = vert_neighbors[1]
2285 else:
2286 first_vert_U_idx = vert_neighbors[0]
2288 if selection_type == "TWO_NOT_CONNECTED":
2289 self.selection_U2_exists = True
2290 # If the second selection is not closed
2291 if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or \
2292 nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx:
2294 self.selection_U2_is_closed = False
2295 closing_vert_U2_idx = None
2296 first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2297 else:
2298 self.selection_U2_is_closed = True
2299 closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2301 # Get the neighbors of the first (unselected) vert of the closed selection U
2302 vert_neighbors = []
2303 for verts in single_unselected_verts_and_neighbors:
2304 if verts[0] == nearest_tip_to_last_st_first_pt_idx:
2305 vert_neighbors.append(verts[1])
2306 vert_neighbors.append(verts[2])
2307 break
2309 points_first_and_neighbor = []
2310 points_first_and_neighbor.append(
2311 self.main_object.matrix_world @
2312 self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co
2314 points_first_and_neighbor.append(
2315 self.main_object.matrix_world @
2316 self.main_object.data.vertices[vert_neighbors[0]].co
2318 points_last_stroke_tips = []
2319 points_last_stroke_tips.append(
2320 self.main_splines.data.splines[
2321 len(self.main_splines.data.splines) - 1
2322 ].bezier_points[0].co
2324 points_last_stroke_tips.append(
2325 self.main_splines.data.splines[
2326 len(self.main_splines.data.splines) - 1
2327 ].bezier_points[1].co
2329 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2330 vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
2332 # Compare the direction of the selection and the last grease pencil stroke to
2333 # determine which is the vertex neighbor to the first vertex (unselected) of
2334 # the closed selection. This will determine the direction of the closed selection
2335 if vec_A.dot(vec_B) < 0:
2336 first_vert_U2_idx = vert_neighbors[1]
2337 else:
2338 first_vert_U2_idx = vert_neighbors[0]
2339 else:
2340 self.selection_U2_exists = False
2342 elif selection_type == "NO_SELECTION":
2343 self.selection_U_exists = False
2344 self.selection_V_exists = False
2346 # Get an ordered list of the vertices of Selection-U
2347 verts_ordered_U = []
2348 if self.selection_U_exists:
2349 verts_ordered_U = self.get_ordered_verts(
2350 self.main_object, all_selected_edges_idx,
2351 all_verts_idx, first_vert_U_idx,
2352 middle_vertex_idx, closing_vert_U_idx
2355 # Get an ordered list of the vertices of Selection-U2
2356 verts_ordered_U2 = []
2357 if self.selection_U2_exists:
2358 verts_ordered_U2 = self.get_ordered_verts(
2359 self.main_object, all_selected_edges_idx,
2360 all_verts_idx, first_vert_U2_idx,
2361 middle_vertex_idx, closing_vert_U2_idx
2364 # Get an ordered list of the vertices of Selection-V
2365 verts_ordered_V = []
2366 if self.selection_V_exists:
2367 verts_ordered_V = self.get_ordered_verts(
2368 self.main_object, all_selected_edges_idx,
2369 all_verts_idx, first_vert_V_idx,
2370 middle_vertex_idx, closing_vert_V_idx
2372 verts_ordered_V_indices = [x.index for x in verts_ordered_V]
2374 # Get an ordered list of the vertices of Selection-V2
2375 verts_ordered_V2 = []
2376 if self.selection_V2_exists:
2377 verts_ordered_V2 = self.get_ordered_verts(
2378 self.main_object, all_selected_edges_idx,
2379 all_verts_idx, first_vert_V2_idx,
2380 middle_vertex_idx, closing_vert_V2_idx
2383 # Check if when there are two-not-connected selections both have the same
2384 # number of verts. If not terminate the script
2385 if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or
2386 (self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
2387 # Display a warning
2388 self.report({'WARNING'}, "Both selections must have the same number of edges")
2390 self.stopping_errors = True
2392 return{'CANCELLED'}
2394 # Calculate edges U proportions
2395 # Sum selected edges U lengths
2396 edges_lengths_U = []
2397 edges_lengths_sum_U = 0
2399 if self.selection_U_exists:
2400 edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(
2401 self.main_object,
2402 verts_ordered_U
2404 if self.selection_U2_exists:
2405 edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(
2406 self.main_object,
2407 verts_ordered_U2
2409 # Sum selected edges V lengths
2410 edges_lengths_V = []
2411 edges_lengths_sum_V = 0
2413 if self.selection_V_exists:
2414 edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(
2415 self.main_object,
2416 verts_ordered_V
2418 if self.selection_V2_exists:
2419 edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(
2420 self.main_object,
2421 verts_ordered_V2
2424 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2425 bpy.ops.curve.subdivide('INVOKE_REGION_WIN',
2426 number_cuts=bpy.context.scene.bsurfaces.SURFSK_precision)
2427 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2429 # Proportions U
2430 edges_proportions_U = []
2431 edges_proportions_U = self.get_edges_proportions(
2432 edges_lengths_U, edges_lengths_sum_U,
2433 self.selection_U_exists, self.edges_U
2435 verts_count_U = len(edges_proportions_U) + 1
2437 if self.selection_U2_exists:
2438 edges_proportions_U2 = []
2439 edges_proportions_U2 = self.get_edges_proportions(
2440 edges_lengths_U2, edges_lengths_sum_U2,
2441 self.selection_U2_exists, self.edges_V
2444 # Proportions V
2445 edges_proportions_V = []
2446 edges_proportions_V = self.get_edges_proportions(
2447 edges_lengths_V, edges_lengths_sum_V,
2448 self.selection_V_exists, self.edges_V
2451 if self.selection_V2_exists:
2452 edges_proportions_V2 = []
2453 edges_proportions_V2 = self.get_edges_proportions(
2454 edges_lengths_V2, edges_lengths_sum_V2,
2455 self.selection_V2_exists, self.edges_V
2458 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2459 # the actual sketched curves with a "closing segment"
2460 if self.cyclic_follow and not self.selection_V_exists and not \
2461 ((self.selection_U_exists and not self.selection_U_is_closed) or
2462 (self.selection_U2_exists and not self.selection_U2_is_closed)):
2464 simplified_spline_coords = []
2465 simplified_curve = []
2466 ob_simplified_curve = []
2467 splines_first_v_co = []
2468 for i in range(len(self.main_splines.data.splines)):
2469 # Create a curve object for the actual spline "cyclic extension"
2470 simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
2471 ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
2472 bpy.context.collection.objects.link(ob_simplified_curve[i])
2474 simplified_curve[i].dimensions = "3D"
2476 spline_coords = []
2477 for bp in self.main_splines.data.splines[i].bezier_points:
2478 spline_coords.append(bp.co)
2480 # Simplification
2481 simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
2483 # Get the coordinates of the first vert of the actual spline
2484 splines_first_v_co.append(simplified_spline_coords[i][0])
2486 # Generate the spline
2487 spline = simplified_curve[i].splines.new('BEZIER')
2488 # less one because one point is added when the spline is created
2489 spline.bezier_points.add(len(simplified_spline_coords[i]) - 1)
2490 for p in range(0, len(simplified_spline_coords[i])):
2491 spline.bezier_points[p].co = simplified_spline_coords[i][p]
2493 spline.use_cyclic_u = True
2495 spline_bp_count = len(spline.bezier_points)
2497 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2498 ob_simplified_curve[i].select_set(True)
2499 bpy.context.view_layer.objects.active = ob_simplified_curve[i]
2501 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2502 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
2503 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2504 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
2505 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2507 # Select the "closing segment", and subdivide it
2508 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
2509 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
2510 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
2512 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
2513 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
2514 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
2516 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2517 segments = sqrt(
2518 (ob_simplified_curve[i].data.splines[0].bezier_points[0].co -
2519 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length /
2520 self.average_gp_segment_length
2522 for t in range(2):
2523 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=segments)
2525 # Delete the other vertices and make it non-cyclic to
2526 # keep only the needed verts of the "closing segment"
2527 bpy.ops.curve.select_all(action='INVERT')
2528 bpy.ops.curve.delete(type='VERT')
2529 ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
2530 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2532 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2533 first_new_index = len(self.main_splines.data.splines[i].bezier_points)
2534 self.main_splines.data.splines[i].bezier_points.add(
2535 len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1
2537 for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
2538 self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = \
2539 ob_simplified_curve[i].data.splines[0].bezier_points[t].co
2541 # Delete the temporal curve
2542 bpy.ops.object.delete({"selected_objects": [ob_simplified_curve[i]]})
2544 # Get the coords of the points distributed along the sketched strokes,
2545 # with proportions-U of the first selection
2546 pts_on_strokes_with_proportions_U = self.distribute_pts(
2547 self.main_splines.data.splines,
2548 edges_proportions_U
2550 sketched_splines_parsed = []
2552 if self.selection_U2_exists:
2553 # Initialize the multidimensional list with the proportions of all the segments
2554 proportions_loops_crossing_strokes = []
2555 for i in range(len(pts_on_strokes_with_proportions_U)):
2556 proportions_loops_crossing_strokes.append([])
2558 for t in range(len(pts_on_strokes_with_proportions_U[0])):
2559 proportions_loops_crossing_strokes[i].append(None)
2561 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2562 for lp in range(len(pts_on_strokes_with_proportions_U[0])):
2563 loop_segments_lengths = []
2565 for st in range(len(pts_on_strokes_with_proportions_U)):
2566 # When on the first stroke, add the segment from the selection to the dirst stroke
2567 if st == 0:
2568 loop_segments_lengths.append(
2569 ((self.main_object.matrix_world @ verts_ordered_U[lp].co) -
2570 pts_on_strokes_with_proportions_U[0][lp]).length
2572 # For all strokes except for the last, calculate the distance
2573 # from the actual stroke to the next
2574 if st != len(pts_on_strokes_with_proportions_U) - 1:
2575 loop_segments_lengths.append(
2576 (pts_on_strokes_with_proportions_U[st][lp] -
2577 pts_on_strokes_with_proportions_U[st + 1][lp]).length
2579 # When on the last stroke, add the segments
2580 # from the last stroke to the second selection
2581 if st == len(pts_on_strokes_with_proportions_U) - 1:
2582 loop_segments_lengths.append(
2583 (pts_on_strokes_with_proportions_U[st][lp] -
2584 (self.main_object.matrix_world @ verts_ordered_U2[lp].co)).length
2586 # Calculate full loop length
2587 loop_seg_lengths_sum = 0
2588 for i in range(len(loop_segments_lengths)):
2589 loop_seg_lengths_sum += loop_segments_lengths[i]
2591 # Fill the multidimensional list with the proportions of all the segments
2592 for st in range(len(pts_on_strokes_with_proportions_U)):
2593 proportions_loops_crossing_strokes[st][lp] = \
2594 loop_segments_lengths[st] / loop_seg_lengths_sum
2596 # Calculate proportions for each stroke
2597 for st in range(len(pts_on_strokes_with_proportions_U)):
2598 actual_stroke_spline = []
2599 # Needs to be a list for the "distribute_pts" method
2600 actual_stroke_spline.append(self.main_splines.data.splines[st])
2602 # Calculate the proportions for the actual stroke.
2603 actual_edges_proportions_U = []
2604 for i in range(len(edges_proportions_U)):
2605 proportions_sum = 0
2607 # Sum the proportions of this loop up to the actual.
2608 for t in range(0, st + 1):
2609 proportions_sum += proportions_loops_crossing_strokes[t][i]
2610 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2611 # and the proportions refer to edges, so we start at the element 1
2612 # of proportions_loops_crossing_strokes instead of element 0
2613 actual_edges_proportions_U.append(
2614 edges_proportions_U[i] -
2615 ((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)
2617 points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
2618 sketched_splines_parsed.append(points_actual_spline[0])
2619 else:
2620 sketched_splines_parsed = pts_on_strokes_with_proportions_U
2622 # If the selection type is "TWO_NOT_CONNECTED" replace the
2623 # points of the last spline with the points in the "target" selection
2624 if selection_type == "TWO_NOT_CONNECTED":
2625 if self.selection_U2_exists:
2626 for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
2627 sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = \
2628 self.main_object.matrix_world @ verts_ordered_U2[i].co
2630 # Create temporary curves along the "control-points" found
2631 # on the sketched curves and the mesh selection
2632 mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
2633 me = bpy.data.meshes.new(mesh_ctrl_pts_name)
2634 ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
2635 ob_ctrl_pts.data = me
2636 bpy.context.collection.objects.link(ob_ctrl_pts)
2638 cyclic_loops_U = []
2639 first_verts = []
2640 second_verts = []
2641 last_verts = []
2643 for i in range(0, verts_count_U):
2644 vert_num_in_spline = 1
2646 if self.selection_U_exists:
2647 ob_ctrl_pts.data.vertices.add(1)
2648 last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2649 last_v.co = self.main_object.matrix_world @ verts_ordered_U[i].co
2651 vert_num_in_spline += 1
2653 for t in range(0, len(sketched_splines_parsed)):
2654 ob_ctrl_pts.data.vertices.add(1)
2655 v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2656 v.co = sketched_splines_parsed[t][i]
2658 if vert_num_in_spline > 1:
2659 ob_ctrl_pts.data.edges.add(1)
2660 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = \
2661 len(ob_ctrl_pts.data.vertices) - 2
2662 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = \
2663 len(ob_ctrl_pts.data.vertices) - 1
2665 if t == 0:
2666 first_verts.append(v.index)
2668 if t == 1:
2669 second_verts.append(v.index)
2671 if t == len(sketched_splines_parsed) - 1:
2672 last_verts.append(v.index)
2674 last_v = v
2675 vert_num_in_spline += 1
2677 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2678 ob_ctrl_pts.select_set(True)
2679 bpy.context.view_layer.objects.active = ob_ctrl_pts
2681 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2682 bpy.ops.mesh.select_all(action='DESELECT')
2683 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2685 # Determine which loops-U will be "Cyclic"
2686 for i in range(0, len(first_verts)):
2687 # When there is Cyclic Cross there is no need of
2688 # Automatic Join, (and there are at least three strokes)
2689 if self.automatic_join and not self.cyclic_cross and \
2690 selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3:
2692 v = ob_ctrl_pts.data.vertices
2693 first_point_co = v[first_verts[i]].co
2694 second_point_co = v[second_verts[i]].co
2695 last_point_co = v[last_verts[i]].co
2697 # Coordinates of the point in the center of both the first and last verts.
2698 verts_center_co = [
2699 (first_point_co[0] + last_point_co[0]) / 2,
2700 (first_point_co[1] + last_point_co[1]) / 2,
2701 (first_point_co[2] + last_point_co[2]) / 2
2703 vec_A = second_point_co - first_point_co
2704 vec_B = second_point_co - Vector(verts_center_co)
2706 # Calculate the length of the first segment of the loop,
2707 # and the length it would have after moving the first vert
2708 # to the middle position between first and last
2709 length_original = (second_point_co - first_point_co).length
2710 length_target = (second_point_co - Vector(verts_center_co)).length
2712 angle = vec_A.angle(vec_B) / pi
2714 # If the target length doesn't stretch too much, and the
2715 # its angle doesn't change to much either
2716 if length_target <= length_original * 1.03 * self.join_stretch_factor and \
2717 angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists:
2719 cyclic_loops_U.append(True)
2720 # Move the first vert to the center coordinates
2721 ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
2722 # Select the last verts from Cyclic loops, for later deletion all at once
2723 v[last_verts[i]].select = True
2724 else:
2725 cyclic_loops_U.append(False)
2726 else:
2727 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2728 if self.cyclic_cross and not self.selection_U_exists and not \
2729 ((self.selection_V_exists and not self.selection_V_is_closed) or
2730 (self.selection_V2_exists and not self.selection_V2_is_closed)):
2732 cyclic_loops_U.append(True)
2733 else:
2734 cyclic_loops_U.append(False)
2736 # The cyclic_loops_U list needs to be reversed.
2737 cyclic_loops_U.reverse()
2739 # Delete the previously selected (last_)verts.
2740 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2741 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
2742 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2744 # Create curves from control points.
2745 bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
2746 ob_curves_surf = bpy.context.view_layer.objects.active
2747 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2748 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2749 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2751 # Make Cyclic the splines designated as Cyclic.
2752 for i in range(0, len(cyclic_loops_U)):
2753 ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
2755 # Get the coords of all points on first loop-U, for later comparison with its
2756 # subdivided version, to know which points of the loops-U are crossed by the
2757 # original strokes. The indices will be the same for the other loops-U
2758 if self.loops_on_strokes:
2759 coords_loops_U_control_points = []
2760 for p in ob_ctrl_pts.data.splines[0].bezier_points:
2761 coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
2763 tuple(coords_loops_U_control_points)
2765 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2766 if self.loops_on_strokes and not self.selection_V_exists:
2767 edges_V_count = len(self.main_splines.data.splines) * self.edges_V
2768 else:
2769 edges_V_count = len(edges_proportions_V)
2771 # The Follow precision will vary depending on the number of Follow face-loops
2772 precision_multiplier = round(2 + (edges_V_count / 15))
2773 curve_cuts = bpy.context.scene.bsurfaces.SURFSK_precision * precision_multiplier
2775 # Subdivide the curves
2776 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=curve_cuts)
2778 # The verts position shifting that happens with splines subdivision.
2779 # For later reorder splines points
2780 verts_position_shift = curve_cuts + 1
2781 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2783 # Reorder coordinates of the points of each spline to put the first point of
2784 # the spline starting at the position it was the first point before sudividing
2785 # the curve. And make a new curve object per spline (to handle memory better later)
2786 splines_U_objects = []
2787 for i in range(len(ob_curves_surf.data.splines)):
2788 spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
2789 ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
2790 bpy.context.collection.objects.link(ob_spline_U)
2792 spline_U_curve.dimensions = "3D"
2794 # Add points to the spline in the new curve object
2795 ob_spline_U.data.splines.new('BEZIER')
2796 for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
2797 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2798 if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
2799 point_index = t + verts_position_shift
2800 else:
2801 point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
2802 else:
2803 point_index = t
2804 # to avoid adding the first point since it's added when the spline is created
2805 if t > 0:
2806 ob_spline_U.data.splines[0].bezier_points.add(1)
2807 ob_spline_U.data.splines[0].bezier_points[t].co = \
2808 ob_curves_surf.data.splines[i].bezier_points[point_index].co
2810 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2811 # Add a last point at the same location as the first one
2812 ob_spline_U.data.splines[0].bezier_points.add(1)
2813 ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = \
2814 ob_spline_U.data.splines[0].bezier_points[0].co
2815 else:
2816 ob_spline_U.data.splines[0].use_cyclic_u = False
2818 splines_U_objects.append(ob_spline_U)
2819 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2820 ob_spline_U.select_set(True)
2821 bpy.context.view_layer.objects.active = ob_spline_U
2823 # When option "Loops on strokes" is active each "Cross" loop will have
2824 # its own proportions according to where the original strokes "touch" them
2825 if self.loops_on_strokes:
2826 # Get the indices of points where the original strokes "touch" loops-U
2827 points_U_crossed_by_strokes = []
2828 for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
2829 bp = splines_U_objects[0].data.splines[0].bezier_points[i]
2830 if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
2831 points_U_crossed_by_strokes.append(i)
2833 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2834 edge_order_number_for_splines = {}
2835 if self.selection_V_exists:
2836 # For two-connected selections add a first hypothetic stroke at the beginning.
2837 if selection_type == "TWO_CONNECTED":
2838 edge_order_number_for_splines[0] = 0
2840 for i in range(len(self.main_splines.data.splines)):
2841 sp = self.main_splines.data.splines[i]
2842 v_idx, _dist_temp = self.shortest_distance(
2843 self.main_object,
2844 sp.bezier_points[0].co,
2845 verts_ordered_V_indices
2847 # Get the position (edges count) of the vert v_idx in the selected chain V
2848 edge_idx_in_chain = verts_ordered_V_indices.index(v_idx)
2850 # For two-connected selections the strokes go after the
2851 # hypothetic stroke added before, so the index adds one per spline
2852 if selection_type == "TWO_CONNECTED":
2853 spline_number = i + 1
2854 else:
2855 spline_number = i
2857 edge_order_number_for_splines[spline_number] = edge_idx_in_chain
2859 # Get the first and last verts indices for later comparison
2860 if i == 0:
2861 first_v_idx = v_idx
2862 elif i == len(self.main_splines.data.splines) - 1:
2863 last_v_idx = v_idx
2865 if self.selection_V_is_closed:
2866 # If there is no last stroke on the last vertex (same as first vertex),
2867 # add a hypothetic spline at last vert order
2868 if first_v_idx != last_v_idx:
2869 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2870 len(verts_ordered_V_indices) - 1
2871 else:
2872 if self.cyclic_cross:
2873 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2874 len(verts_ordered_V_indices) - 2
2875 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2876 len(verts_ordered_V_indices) - 1
2877 else:
2878 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2879 len(verts_ordered_V_indices) - 1
2881 # Get the coords of the points distributed along the
2882 # "crossing curves", with appropriate proportions-V
2883 surface_splines_parsed = []
2884 for i in range(len(splines_U_objects)):
2885 sp_ob = splines_U_objects[i]
2886 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2887 if self.loops_on_strokes:
2888 # Segments distances from stroke to stroke
2889 dist = 0
2890 full_dist = 0
2891 segments_distances = []
2892 for t in range(len(sp_ob.data.splines[0].bezier_points)):
2893 bp = sp_ob.data.splines[0].bezier_points[t]
2895 if t == 0:
2896 last_p = bp.co
2897 else:
2898 actual_p = bp.co
2899 dist += (last_p - actual_p).length
2901 if t in points_U_crossed_by_strokes:
2902 segments_distances.append(dist)
2903 full_dist += dist
2905 dist = 0
2907 last_p = actual_p
2909 # Calculate Proportions.
2910 used_edges_proportions_V = []
2911 for t in range(len(segments_distances)):
2912 if self.selection_V_exists:
2913 if t == 0:
2914 order_number_last_stroke = 0
2916 segment_edges_length_V = 0
2917 segment_edges_length_V2 = 0
2918 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2919 segment_edges_length_V += edges_lengths_V[order]
2920 if self.selection_V2_exists:
2921 segment_edges_length_V2 += edges_lengths_V2[order]
2923 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2924 # Calculate each "sub-segment" (the ones between each stroke) length
2925 if self.selection_V2_exists:
2926 proportion_sub_seg = (edges_lengths_V2[order] -
2927 ((edges_lengths_V2[order] - edges_lengths_V[order]) /
2928 len(splines_U_objects) * i)) / (segment_edges_length_V2 -
2929 (segment_edges_length_V2 - segment_edges_length_V) /
2930 len(splines_U_objects) * i)
2932 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2933 else:
2934 proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
2935 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2937 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2939 order_number_last_stroke = edge_order_number_for_splines[t + 1]
2941 else:
2942 for _c in range(self.edges_V):
2943 # Calculate each "sub-segment" (the ones between each stroke) length
2944 sub_seg_dist = segments_distances[t] / self.edges_V
2945 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2947 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2948 surface_splines_parsed.append(actual_spline[0])
2950 else:
2951 if self.selection_V2_exists:
2952 used_edges_proportions_V = []
2953 for p in range(len(edges_proportions_V)):
2954 used_edges_proportions_V.append(
2955 edges_proportions_V2[p] -
2956 ((edges_proportions_V2[p] -
2957 edges_proportions_V[p]) / len(splines_U_objects) * i)
2959 else:
2960 used_edges_proportions_V = edges_proportions_V
2962 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2963 surface_splines_parsed.append(actual_spline[0])
2965 # Set the verts of the first and last splines to the locations
2966 # of the respective verts in the selections
2967 if self.selection_V_exists:
2968 for i in range(0, len(surface_splines_parsed[0])):
2969 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = \
2970 self.main_object.matrix_world @ verts_ordered_V[i].co
2972 if selection_type == "TWO_NOT_CONNECTED":
2973 if self.selection_V2_exists:
2974 for i in range(0, len(surface_splines_parsed[0])):
2975 surface_splines_parsed[0][i] = self.main_object.matrix_world @ verts_ordered_V2[i].co
2977 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2978 # merge the verts of the tips of the loops when they are "near enough"
2979 if self.automatic_join and selection_type != "TWO_CONNECTED":
2980 # Join the tips of "Follow" loops that are near enough and must be "closed"
2981 if not self.selection_V_exists and len(edges_proportions_U) >= 3:
2982 for i in range(len(surface_splines_parsed[0])):
2983 sp = surface_splines_parsed
2984 loop_segment_dist = (sp[0][i] - sp[1][i]).length
2986 verts_middle_position_co = [
2987 (sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2,
2988 (sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2,
2989 (sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2
2991 points_original = []
2992 points_original.append(sp[1][i])
2993 points_original.append(sp[0][i])
2995 points_target = []
2996 points_target.append(sp[1][i])
2997 points_target.append(Vector(verts_middle_position_co))
2999 vec_A = points_original[0] - points_original[1]
3000 vec_B = points_target[0] - points_target[1]
3001 # check for zero angles, not sure if it is a great fix
3002 if vec_A.length != 0 and vec_B.length != 0:
3003 angle = vec_A.angle(vec_B) / pi
3004 edge_new_length = (Vector(verts_middle_position_co) - sp[1][i]).length
3005 else:
3006 angle = 0
3007 edge_new_length = 0
3009 # If after moving the verts to the middle point, the segment doesn't stretch too much
3010 if edge_new_length <= loop_segment_dist * 1.5 * \
3011 self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor:
3013 # Avoid joining when the actual loop must be merged with the original mesh
3014 if not (self.selection_U_exists and i == 0) and \
3015 not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1):
3017 # Change the coords of both verts to the middle position
3018 surface_splines_parsed[0][i] = verts_middle_position_co
3019 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
3021 # Delete object with control points and object from grease pencil conversion
3022 bpy.ops.object.delete({"selected_objects": [ob_ctrl_pts]})
3024 bpy.ops.object.delete({"selected_objects": splines_U_objects})
3026 # Generate surface
3028 # Get all verts coords
3029 all_surface_verts_co = []
3030 for i in range(0, len(surface_splines_parsed)):
3031 # Get coords of all verts and make a list with them
3032 for pt_co in surface_splines_parsed[i]:
3033 all_surface_verts_co.append(pt_co)
3035 # Define verts for each face
3036 all_surface_faces = []
3037 for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
3038 if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
3039 all_surface_faces.append(
3040 [i + 1, i, i + len(surface_splines_parsed[0]),
3041 i + len(surface_splines_parsed[0]) + 1]
3043 # Build the mesh
3044 surf_me_name = "SURFSKIO_surface"
3045 me_surf = bpy.data.meshes.new(surf_me_name)
3046 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
3047 ob_surface = object_utils.object_data_add(context, me_surf)
3048 ob_surface.location = (0.0, 0.0, 0.0)
3049 ob_surface.rotation_euler = (0.0, 0.0, 0.0)
3050 ob_surface.scale = (1.0, 1.0, 1.0)
3052 # Select all the "unselected but participating" verts, from closed selection
3053 # or double selections with middle-vertex, for later join with remove doubles
3054 for v_idx in single_unselected_verts:
3055 self.main_object.data.vertices[v_idx].select = True
3057 # Join the new mesh to the main object
3058 ob_surface.select_set(True)
3059 self.main_object.select_set(True)
3060 bpy.context.view_layer.objects.active = self.main_object
3062 bpy.ops.object.join('INVOKE_REGION_WIN')
3064 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3066 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
3067 bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
3068 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
3070 self.update()
3072 return{'FINISHED'}
3074 def update(self):
3075 try:
3076 global global_offset
3077 shrinkwrap = self.main_object.modifiers["Shrinkwrap"]
3078 shrinkwrap.offset = global_offset
3079 bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset = global_offset
3080 except:
3081 pass
3083 try:
3084 global global_color
3085 material = makeMaterial("BSurfaceMesh", global_color)
3086 if self.main_object.data.materials:
3087 self.main_object.data.materials[0] = material
3088 else:
3089 self.main_object.data.materials.append(material)
3090 bpy.context.scene.bsurfaces.SURFSK_mesh_color = global_color
3091 except:
3092 pass
3094 try:
3095 global global_in_front
3096 self.main_object.show_in_front = global_in_front
3097 bpy.context.scene.bsurfaces.SURFSK_in_front = global_in_front
3098 except:
3099 pass
3101 try:
3102 global global_show_wire
3103 self.main_object.show_wire = global_show_wire
3104 bpy.context.scene.bsurfaces.SURFSK_show_wire = global_show_wire
3105 except:
3106 pass
3108 try:
3109 global global_shade_smooth
3110 if global_shade_smooth:
3111 bpy.ops.object.shade_smooth()
3112 else:
3113 bpy.ops.object.shade_flat()
3114 bpy.context.scene.bsurfaces.SURFSK_shade_smooth = global_shade_smooth
3115 except:
3116 pass
3118 return{'FINISHED'}
3120 def execute(self, context):
3122 if bpy.ops.object.mode_set.poll():
3123 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3125 try:
3126 global global_mesh_object
3127 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3128 bpy.data.objects[global_mesh_object].select_set(True)
3129 self.main_object = bpy.data.objects[global_mesh_object]
3130 bpy.context.view_layer.objects.active = self.main_object
3131 bsurfaces_props = bpy.context.scene.bsurfaces
3132 except:
3133 self.report({'WARNING'}, "Specify the name of the object with retopology")
3134 return{"CANCELLED"}
3135 bpy.context.view_layer.objects.active = self.main_object
3137 self.update()
3139 if not self.is_fill_faces:
3140 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3141 value='True, False, False')
3143 # Build splines from the "last saved splines".
3144 last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
3145 self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
3146 bpy.context.collection.objects.link(self.main_splines)
3148 last_saved_curve.dimensions = "3D"
3150 for sp in self.last_strokes_splines_coords:
3151 spline = self.main_splines.data.splines.new('BEZIER')
3152 # less one because one point is added when the spline is created
3153 spline.bezier_points.add(len(sp) - 1)
3154 for p in range(0, len(sp)):
3155 spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
3157 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3159 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3160 self.main_splines.select_set(True)
3161 bpy.context.view_layer.objects.active = self.main_splines
3163 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3165 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3166 # Important to make it vector first and then automatic, otherwise the
3167 # tips handles get too big and distort the shrinkwrap results later
3168 bpy.ops.curve.handle_type_set(type='VECTOR')
3169 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3170 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3171 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3173 self.main_splines.name = "SURFSKIO_temp_strokes"
3175 if self.is_crosshatch:
3176 strokes_for_crosshatch = True
3177 strokes_for_rectangular_surface = False
3178 else:
3179 strokes_for_rectangular_surface = True
3180 strokes_for_crosshatch = False
3182 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3184 if strokes_for_rectangular_surface:
3185 self.rectangular_surface(context)
3186 elif strokes_for_crosshatch:
3187 self.crosshatch_surface_execute(context)
3189 #Set Shade smooth to new polygons
3190 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3191 global global_shade_smooth
3192 if global_shade_smooth:
3193 bpy.ops.object.shade_smooth()
3194 else:
3195 bpy.ops.object.shade_flat()
3197 # Delete main splines
3198 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3199 if self.keep_strokes:
3200 self.main_splines.name = "keep_strokes"
3201 self.main_splines.data.bevel_depth = 0.001
3202 if "keep_strokes_material" in bpy.data.materials :
3203 self.main_splines.data.materials.append(bpy.data.materials["keep_strokes_material"])
3204 else:
3205 mat = bpy.data.materials.new("keep_strokes_material")
3206 mat.diffuse_color = (1, 0, 0, 0)
3207 mat.specular_color = (1, 0, 0)
3208 mat.specular_intensity = 0.0
3209 mat.roughness = 0.0
3210 self.main_splines.data.materials.append(mat)
3211 else:
3212 bpy.ops.object.delete({"selected_objects": [self.main_splines]})
3214 # Delete grease pencil strokes
3215 if self.strokes_type == "GP_STROKES" and not self.stopping_errors:
3216 try:
3217 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3218 except:
3219 pass
3221 # Delete annotations
3222 if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors:
3223 try:
3224 bpy.context.annotation_data.layers.active.clear()
3225 except:
3226 pass
3228 bsurfaces_props = bpy.context.scene.bsurfaces
3229 bsurfaces_props.SURFSK_edges_U = self.edges_U
3230 bsurfaces_props.SURFSK_edges_V = self.edges_V
3231 bsurfaces_props.SURFSK_cyclic_cross = self.cyclic_cross
3232 bsurfaces_props.SURFSK_cyclic_follow = self.cyclic_follow
3233 bsurfaces_props.SURFSK_automatic_join = self.automatic_join
3234 bsurfaces_props.SURFSK_loops_on_strokes = self.loops_on_strokes
3235 bsurfaces_props.SURFSK_keep_strokes = self.keep_strokes
3237 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3238 self.main_object.select_set(True)
3239 bpy.context.view_layer.objects.active = self.main_object
3241 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3243 self.update()
3245 return{'FINISHED'}
3247 def invoke(self, context, event):
3249 if bpy.ops.object.mode_set.poll():
3250 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3252 bsurfaces_props = bpy.context.scene.bsurfaces
3253 self.cyclic_cross = bsurfaces_props.SURFSK_cyclic_cross
3254 self.cyclic_follow = bsurfaces_props.SURFSK_cyclic_follow
3255 self.automatic_join = bsurfaces_props.SURFSK_automatic_join
3256 self.loops_on_strokes = bsurfaces_props.SURFSK_loops_on_strokes
3257 self.keep_strokes = bsurfaces_props.SURFSK_keep_strokes
3259 try:
3260 global global_mesh_object
3261 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3262 bpy.data.objects[global_mesh_object].select_set(True)
3263 self.main_object = bpy.data.objects[global_mesh_object]
3264 bpy.context.view_layer.objects.active = self.main_object
3265 except:
3266 self.report({'WARNING'}, "Specify the name of the object with retopology")
3267 return{"CANCELLED"}
3269 self.update()
3271 self.main_object_selected_verts_count = len([v for v in self.main_object.data.vertices if v.select])
3273 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3274 value='True, False, False')
3276 self.edges_U = bsurfaces_props.SURFSK_edges_U
3277 self.edges_V = bsurfaces_props.SURFSK_edges_V
3279 self.is_fill_faces = False
3280 self.stopping_errors = False
3281 self.last_strokes_splines_coords = []
3283 # Determine the type of the strokes
3284 self.strokes_type = get_strokes_type(context)
3286 # Check if it will be used grease pencil strokes or curves
3287 # If there are strokes to be used
3288 if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "GP_ANNOTATION":
3289 if self.strokes_type == "GP_STROKES":
3290 # Convert grease pencil strokes to curve
3291 global global_gpencil_object
3292 gp = bpy.data.objects[global_gpencil_object]
3293 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3294 self.using_external_curves = False
3296 elif self.strokes_type == "GP_ANNOTATION":
3297 # Convert grease pencil strokes to curve
3298 gp = bpy.context.annotation_data
3299 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'Annotation')
3300 self.using_external_curves = False
3302 elif self.strokes_type == "EXTERNAL_CURVE":
3303 global global_curve_object
3304 self.original_curve = bpy.data.objects[global_curve_object]
3305 self.using_external_curves = True
3307 # Make sure there are no objects left from erroneous
3308 # executions of this operator, with the reserved names used here
3309 for o in bpy.data.objects:
3310 if o.name.find("SURFSKIO_") != -1:
3311 bpy.ops.object.delete({"selected_objects": [o]})
3313 bpy.context.view_layer.objects.active = self.original_curve
3315 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3317 self.temporary_curve = bpy.context.view_layer.objects.active
3319 # Deselect all points of the curve
3320 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3321 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3322 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3324 # Delete splines with only a single isolated point
3325 for i in range(len(self.temporary_curve.data.splines)):
3326 sp = self.temporary_curve.data.splines[i]
3328 if len(sp.bezier_points) == 1:
3329 sp.bezier_points[0].select_control_point = True
3331 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3332 bpy.ops.curve.delete(type='VERT')
3333 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3335 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3336 self.temporary_curve.select_set(True)
3337 bpy.context.view_layer.objects.active = self.temporary_curve
3339 # Set a minimum number of points for crosshatch
3340 minimum_points_num = 15
3342 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3343 # Check if the number of points of each curve has at least the number of points
3344 # of minimum_points_num, which is a bit more than the face-loops limit.
3345 # If not, subdivide to reach at least that number of points
3346 for i in range(len(self.temporary_curve.data.splines)):
3347 sp = self.temporary_curve.data.splines[i]
3349 if len(sp.bezier_points) < minimum_points_num:
3350 for bp in sp.bezier_points:
3351 bp.select_control_point = True
3353 if (len(sp.bezier_points) - 1) != 0:
3354 # Formula to get the number of cuts that will make a curve
3355 # of N number of points have near to "minimum_points_num"
3356 # points, when subdividing with this number of cuts
3357 subdivide_cuts = int(
3358 (minimum_points_num - len(sp.bezier_points)) /
3359 (len(sp.bezier_points) - 1)
3360 ) + 1
3361 else:
3362 subdivide_cuts = 0
3364 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3365 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3367 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3369 # Detect if the strokes are a crosshatch and do it if it is
3370 self.crosshatch_surface_invoke(self.temporary_curve)
3372 if not self.is_crosshatch:
3373 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3374 self.temporary_curve.select_set(True)
3375 bpy.context.view_layer.objects.active = self.temporary_curve
3377 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3379 # Set a minimum number of points for rectangular surfaces
3380 minimum_points_num = 60
3382 # Check if the number of points of each curve has at least the number of points
3383 # of minimum_points_num, which is a bit more than the face-loops limit.
3384 # If not, subdivide to reach at least that number of points
3385 for i in range(len(self.temporary_curve.data.splines)):
3386 sp = self.temporary_curve.data.splines[i]
3388 if len(sp.bezier_points) < minimum_points_num:
3389 for bp in sp.bezier_points:
3390 bp.select_control_point = True
3392 if (len(sp.bezier_points) - 1) != 0:
3393 # Formula to get the number of cuts that will make a curve of
3394 # N number of points have near to "minimum_points_num" points,
3395 # when subdividing with this number of cuts
3396 subdivide_cuts = int(
3397 (minimum_points_num - len(sp.bezier_points)) /
3398 (len(sp.bezier_points) - 1)
3399 ) + 1
3400 else:
3401 subdivide_cuts = 0
3403 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3404 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3406 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3408 # Save coordinates of the actual strokes (as the "last saved splines")
3409 for sp_idx in range(len(self.temporary_curve.data.splines)):
3410 self.last_strokes_splines_coords.append([])
3411 for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
3412 coords = self.temporary_curve.matrix_world @ \
3413 self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
3414 self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
3416 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3417 for sp_idx in range(len(self.temporary_curve.data.splines)):
3418 if self.temporary_curve.data.splines[sp_idx].use_cyclic_u is True:
3419 first_p_co = self.last_strokes_splines_coords[sp_idx][0]
3420 last_p_co = self.last_strokes_splines_coords[sp_idx][
3421 len(self.last_strokes_splines_coords[sp_idx]) - 1
3423 target_co = [
3424 (first_p_co[0] + last_p_co[0]) / 2,
3425 (first_p_co[1] + last_p_co[1]) / 2,
3426 (first_p_co[2] + last_p_co[2]) / 2
3429 self.last_strokes_splines_coords[sp_idx][0] = target_co
3430 self.last_strokes_splines_coords[sp_idx][
3431 len(self.last_strokes_splines_coords[sp_idx]) - 1
3432 ] = target_co
3433 tuple(self.last_strokes_splines_coords)
3435 # Estimation of the average length of the segments between
3436 # each point of the grease pencil strokes.
3437 # Will be useful to determine whether a curve should be made "Cyclic"
3438 segments_lengths_sum = 0
3439 segments_count = 0
3440 random_spline = self.temporary_curve.data.splines[0].bezier_points
3441 for i in range(0, len(random_spline)):
3442 if i != 0 and len(random_spline) - 1 >= i:
3443 segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
3444 segments_count += 1
3446 self.average_gp_segment_length = segments_lengths_sum / segments_count
3448 # Delete temporary strokes curve object
3449 bpy.ops.object.delete({"selected_objects": [self.temporary_curve]})
3451 # Set again since "execute()" will turn it again to its initial value
3452 self.execute(context)
3454 if not self.stopping_errors:
3455 # Delete grease pencil strokes
3456 if self.strokes_type == "GP_STROKES":
3457 try:
3458 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3459 except:
3460 pass
3462 # Delete annotation strokes
3463 elif self.strokes_type == "GP_ANNOTATION":
3464 try:
3465 bpy.context.annotation_data.layers.active.clear()
3466 except:
3467 pass
3469 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3470 bpy.ops.object.delete({"selected_objects": [self.original_curve]})
3471 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3473 return {"FINISHED"}
3474 else:
3475 return{"CANCELLED"}
3477 elif self.strokes_type == "SELECTION_ALONE":
3478 self.is_fill_faces = True
3479 created_faces_count = self.fill_with_faces(self.main_object)
3481 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3483 if created_faces_count == 0:
3484 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3485 return {"CANCELLED"}
3486 else:
3487 return {"FINISHED"}
3489 if self.strokes_type == "EXTERNAL_NO_CURVE":
3490 self.report({'WARNING'}, "The secondary object is not a Curve.")
3491 return{"CANCELLED"}
3493 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3494 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3495 return{"CANCELLED"}
3497 elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or \
3498 self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3500 self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3501 return{"CANCELLED"}
3503 elif self.strokes_type == "NO_STROKES":
3504 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3505 return{"CANCELLED"}
3507 elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
3508 self.report({'WARNING'}, "All splines must be Bezier.")
3509 return{"CANCELLED"}
3511 else:
3512 return{"CANCELLED"}
3514 # ----------------------------
3515 # Init operator
3516 class MESH_OT_SURFSK_init(Operator):
3517 bl_idname = "mesh.surfsk_init"
3518 bl_label = "Bsurfaces initialize"
3519 bl_description = "Add an empty mesh object with useful settings"
3520 bl_options = {'REGISTER', 'UNDO'}
3522 def execute(self, context):
3524 bs = bpy.context.scene.bsurfaces
3526 if bpy.ops.object.mode_set.poll():
3527 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3529 global global_color
3530 global global_offset
3531 global global_in_front
3532 global global_show_wire
3533 global global_shade_smooth
3534 global global_mesh_object
3535 global global_gpencil_object
3537 if bs.SURFSK_mesh == None:
3538 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3539 mesh = bpy.data.meshes.new('BSurfaceMesh')
3540 mesh_object = object_utils.object_data_add(context, mesh)
3541 mesh_object.select_set(True)
3542 bpy.context.view_layer.objects.active = mesh_object
3544 mesh_object.show_all_edges = True
3545 global_in_front = bpy.context.scene.bsurfaces.SURFSK_in_front
3546 mesh_object.show_in_front = global_in_front
3547 mesh_object.display_type = 'SOLID'
3548 mesh_object.show_wire = True
3550 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
3551 if global_shade_smooth:
3552 bpy.ops.object.shade_smooth()
3553 else:
3554 bpy.ops.object.shade_flat()
3556 global_show_wire = bpy.context.scene.bsurfaces.SURFSK_show_wire
3557 mesh_object.show_wire = global_show_wire
3559 global_color = bpy.context.scene.bsurfaces.SURFSK_mesh_color
3560 material = makeMaterial("BSurfaceMesh", global_color)
3561 mesh_object.data.materials.append(material)
3562 bpy.ops.object.modifier_add(type='SHRINKWRAP')
3563 modifier = mesh_object.modifiers["Shrinkwrap"]
3564 if self.active_object is not None:
3565 modifier.target = self.active_object
3566 modifier.wrap_method = 'TARGET_PROJECT'
3567 modifier.wrap_mode = 'OUTSIDE_SURFACE'
3568 modifier.show_on_cage = True
3569 global_offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3570 modifier.offset = global_offset
3572 global_mesh_object = mesh_object.name
3573 bpy.context.scene.bsurfaces.SURFSK_mesh = bpy.data.objects[global_mesh_object]
3575 bpy.context.scene.tool_settings.snap_elements = {'FACE'}
3576 bpy.context.scene.tool_settings.use_snap = True
3577 bpy.context.scene.tool_settings.use_snap_self = False
3578 bpy.context.scene.tool_settings.use_snap_align_rotation = True
3579 bpy.context.scene.tool_settings.use_snap_project = True
3580 bpy.context.scene.tool_settings.use_snap_rotate = True
3581 bpy.context.scene.tool_settings.use_snap_scale = True
3583 bpy.context.scene.tool_settings.use_mesh_automerge = True
3584 bpy.context.scene.tool_settings.double_threshold = 0.01
3586 if context.scene.bsurfaces.SURFSK_guide == 'GPencil' and bs.SURFSK_gpencil == None:
3587 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3588 bpy.ops.object.gpencil_add(radius=1.0, align='WORLD', location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), type='EMPTY')
3589 bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d = 'SURFACE'
3590 gpencil_object = bpy.context.scene.objects[bpy.context.scene.objects[-1].name]
3591 gpencil_object.select_set(True)
3592 bpy.context.view_layer.objects.active = gpencil_object
3593 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3594 global_gpencil_object = gpencil_object.name
3595 bpy.context.scene.bsurfaces.SURFSK_gpencil = bpy.data.objects[global_gpencil_object]
3596 gpencil_object.data.stroke_depth_order = '3D'
3597 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3598 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3600 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
3601 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3602 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3604 def invoke(self, context, event):
3605 if bpy.context.active_object:
3606 self.active_object = bpy.context.active_object
3607 else:
3608 self.active_object = None
3610 self.execute(context)
3612 return {"FINISHED"}
3614 # ----------------------------
3615 # Add modifiers operator
3616 class MESH_OT_SURFSK_add_modifiers(Operator):
3617 bl_idname = "mesh.surfsk_add_modifiers"
3618 bl_label = "Add Mirror and others modifiers"
3619 bl_description = "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3620 bl_options = {'REGISTER', 'UNDO'}
3622 def execute(self, context):
3624 bs = bpy.context.scene.bsurfaces
3626 if bpy.ops.object.mode_set.poll():
3627 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3629 if bs.SURFSK_mesh == None:
3630 self.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3631 else:
3632 mesh_object = bs.SURFSK_mesh
3634 try:
3635 mesh_object.select_set(True)
3636 except:
3637 self.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3638 return {"CANCEL"}
3640 bpy.context.view_layer.objects.active = mesh_object
3642 try:
3643 shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
3644 if self.active_object is not None and self.active_object != mesh_object:
3645 shrinkwrap.target = self.active_object
3646 shrinkwrap.wrap_method = 'TARGET_PROJECT'
3647 shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
3648 shrinkwrap.show_on_cage = True
3649 shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3650 except:
3651 bpy.ops.object.modifier_add(type='SHRINKWRAP')
3652 shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
3653 if self.active_object is not None and self.active_object != mesh_object:
3654 shrinkwrap.target = self.active_object
3655 shrinkwrap.wrap_method = 'TARGET_PROJECT'
3656 shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
3657 shrinkwrap.show_on_cage = True
3658 shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3660 try:
3661 mirror = mesh_object.modifiers["Mirror"]
3662 mirror.use_clip = True
3663 except:
3664 bpy.ops.object.modifier_add(type='MIRROR')
3665 mirror = mesh_object.modifiers["Mirror"]
3666 mirror.use_clip = True
3668 try:
3669 _subsurf = mesh_object.modifiers["Subdivision"]
3670 except:
3671 bpy.ops.object.modifier_add(type='SUBSURF')
3672 _subsurf = mesh_object.modifiers["Subdivision"]
3674 try:
3675 solidify = mesh_object.modifiers["Solidify"]
3676 solidify.thickness = 0.01
3677 except:
3678 bpy.ops.object.modifier_add(type='SOLIDIFY')
3679 solidify = mesh_object.modifiers["Solidify"]
3680 solidify.thickness = 0.01
3682 return {"FINISHED"}
3684 def invoke(self, context, event):
3685 if bpy.context.active_object:
3686 self.active_object = bpy.context.active_object
3687 else:
3688 self.active_object = None
3690 self.execute(context)
3692 return {"FINISHED"}
3694 # ----------------------------
3695 # Edit surface operator
3696 class MESH_OT_SURFSK_edit_surface(Operator):
3697 bl_idname = "mesh.surfsk_edit_surface"
3698 bl_label = "Bsurfaces edit surface"
3699 bl_description = "Edit surface mesh"
3700 bl_options = {'REGISTER', 'UNDO'}
3702 def execute(self, context):
3703 if bpy.ops.object.mode_set.poll():
3704 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3705 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3706 bpy.context.scene.bsurfaces.SURFSK_mesh.select_set(True)
3707 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_mesh
3708 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3709 bpy.ops.wm.tool_set_by_id(name="builtin.select")
3711 def invoke(self, context, event):
3712 try:
3713 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3714 bpy.data.objects[global_mesh_object].select_set(True)
3715 self.main_object = bpy.data.objects[global_mesh_object]
3716 bpy.context.view_layer.objects.active = self.main_object
3717 except:
3718 self.report({'WARNING'}, "Specify the name of the object with retopology")
3719 return{"CANCELLED"}
3721 self.execute(context)
3723 return {"FINISHED"}
3725 # ----------------------------
3726 # Add strokes operator
3727 class GPENCIL_OT_SURFSK_add_strokes(Operator):
3728 bl_idname = "gpencil.surfsk_add_strokes"
3729 bl_label = "Bsurfaces add strokes"
3730 bl_description = "Add the grease pencil strokes"
3731 bl_options = {'REGISTER', 'UNDO'}
3733 def execute(self, context):
3734 if bpy.ops.object.mode_set.poll():
3735 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3736 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3738 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3739 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_gpencil
3740 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3741 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3743 return{"FINISHED"}
3745 def invoke(self, context, event):
3746 try:
3747 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3748 except:
3749 self.report({'WARNING'}, "Specify the name of the object with strokes")
3750 return{"CANCELLED"}
3752 self.execute(context)
3754 return {"FINISHED"}
3756 # ----------------------------
3757 # Edit strokes operator
3758 class GPENCIL_OT_SURFSK_edit_strokes(Operator):
3759 bl_idname = "gpencil.surfsk_edit_strokes"
3760 bl_label = "Bsurfaces edit strokes"
3761 bl_description = "Edit the grease pencil strokes"
3762 bl_options = {'REGISTER', 'UNDO'}
3764 def execute(self, context):
3765 if bpy.ops.object.mode_set.poll():
3766 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3767 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3769 gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil
3771 gpencil_object.select_set(True)
3772 bpy.context.view_layer.objects.active = gpencil_object
3774 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT_GPENCIL')
3775 try:
3776 bpy.ops.gpencil.select_all(action='SELECT')
3777 except:
3778 pass
3780 def invoke(self, context, event):
3781 try:
3782 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3783 except:
3784 self.report({'WARNING'}, "Specify the name of the object with strokes")
3785 return{"CANCELLED"}
3787 self.execute(context)
3789 return {"FINISHED"}
3791 # ----------------------------
3792 # Convert annotation to curves operator
3793 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator):
3794 bl_idname = "gpencil.surfsk_annotations_to_curves"
3795 bl_label = "Convert annotation to curves"
3796 bl_description = "Convert annotation to curves for editing"
3797 bl_options = {'REGISTER', 'UNDO'}
3799 def execute(self, context):
3801 if bpy.ops.object.mode_set.poll():
3802 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3804 # Convert annotation to curve
3805 curve = conver_gpencil_to_curve(self, context, None, 'Annotation')
3807 if curve != None:
3808 # Delete annotation strokes
3809 try:
3810 bpy.context.annotation_data.layers.active.clear()
3811 except:
3812 pass
3814 # Clean up curves
3815 curve.select_set(True)
3816 bpy.context.view_layer.objects.active = curve
3818 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3820 return {"FINISHED"}
3822 def invoke(self, context, event):
3823 try:
3824 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
3826 _strokes_num = len(strokes)
3827 except:
3828 self.report({'WARNING'}, "Not active annotation")
3829 return{"CANCELLED"}
3831 self.execute(context)
3833 return {"FINISHED"}
3835 # ----------------------------
3836 # Convert strokes to curves operator
3837 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator):
3838 bl_idname = "gpencil.surfsk_strokes_to_curves"
3839 bl_label = "Convert strokes to curves"
3840 bl_description = "Convert grease pencil strokes to curves for editing"
3841 bl_options = {'REGISTER', 'UNDO'}
3843 def execute(self, context):
3845 if bpy.ops.object.mode_set.poll():
3846 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3848 # Convert grease pencil strokes to curve
3849 gp = bpy.context.scene.bsurfaces.SURFSK_gpencil
3850 curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3852 if curve != None:
3853 # Delete grease pencil strokes
3854 try:
3855 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3856 except:
3857 pass
3859 # Clean up curves
3861 curve.select_set(True)
3862 bpy.context.view_layer.objects.active = curve
3864 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3866 return {"FINISHED"}
3868 def invoke(self, context, event):
3869 try:
3870 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3871 except:
3872 self.report({'WARNING'}, "Specify the name of the object with strokes")
3873 return{"CANCELLED"}
3875 self.execute(context)
3877 return {"FINISHED"}
3879 # ----------------------------
3880 # Add annotation
3881 class GPENCIL_OT_SURFSK_add_annotation(Operator):
3882 bl_idname = "gpencil.surfsk_add_annotation"
3883 bl_label = "Bsurfaces add annotation"
3884 bl_description = "Add annotation"
3885 bl_options = {'REGISTER', 'UNDO'}
3887 def execute(self, context):
3888 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3889 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3891 return{"FINISHED"}
3893 def invoke(self, context, event):
3895 self.execute(context)
3897 return {"FINISHED"}
3900 # ----------------------------
3901 # Edit curve operator
3902 class CURVE_OT_SURFSK_edit_curve(Operator):
3903 bl_idname = "curve.surfsk_edit_curve"
3904 bl_label = "Bsurfaces edit curve"
3905 bl_description = "Edit curve"
3906 bl_options = {'REGISTER', 'UNDO'}
3908 def execute(self, context):
3909 if bpy.ops.object.mode_set.poll():
3910 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3911 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3912 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3913 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_curve
3914 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3916 def invoke(self, context, event):
3917 try:
3918 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3919 except:
3920 self.report({'WARNING'}, "Specify the name of the object with curve")
3921 return{"CANCELLED"}
3923 self.execute(context)
3925 return {"FINISHED"}
3927 # ----------------------------
3928 # Reorder splines
3929 class CURVE_OT_SURFSK_reorder_splines(Operator):
3930 bl_idname = "curve.surfsk_reorder_splines"
3931 bl_label = "Bsurfaces reorder splines"
3932 bl_description = "Defines the order of the splines by using grease pencil strokes"
3933 bl_options = {'REGISTER', 'UNDO'}
3935 def execute(self, context):
3936 objects_to_delete = []
3937 # Convert grease pencil strokes to curve.
3938 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3939 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3940 for ob in bpy.context.selected_objects:
3941 if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
3942 GP_strokes_curve = ob
3944 # GP_strokes_curve = bpy.context.object
3945 objects_to_delete.append(GP_strokes_curve)
3947 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3948 GP_strokes_curve.select_set(True)
3949 bpy.context.view_layer.objects.active = GP_strokes_curve
3951 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3952 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3953 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=100)
3954 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3956 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3957 GP_strokes_mesh = bpy.context.object
3958 objects_to_delete.append(GP_strokes_mesh)
3960 GP_strokes_mesh.data.resolution_u = 1
3961 bpy.ops.object.convert(target='MESH', keep_original=False)
3963 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3964 self.main_curve.select_set(True)
3965 bpy.context.view_layer.objects.active = self.main_curve
3967 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3968 curves_duplicate_1 = bpy.context.object
3969 objects_to_delete.append(curves_duplicate_1)
3971 minimum_points_num = 500
3973 # Some iterations since the subdivision operator
3974 # has a limit of 100 subdivisions per iteration
3975 for x in range(round(minimum_points_num / 100)):
3976 # Check if the number of points of each curve has at least the number of points
3977 # of minimum_points_num. If not, subdivide to reach at least that number of points
3978 for i in range(len(curves_duplicate_1.data.splines)):
3979 sp = curves_duplicate_1.data.splines[i]
3981 if len(sp.bezier_points) < minimum_points_num:
3982 for bp in sp.bezier_points:
3983 bp.select_control_point = True
3985 if (len(sp.bezier_points) - 1) != 0:
3986 # Formula to get the number of cuts that will make a curve of N
3987 # number of points have near to "minimum_points_num" points,
3988 # when subdividing with this number of cuts
3989 subdivide_cuts = int(
3990 (minimum_points_num - len(sp.bezier_points)) /
3991 (len(sp.bezier_points) - 1)
3992 ) + 1
3993 else:
3994 subdivide_cuts = 0
3996 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3997 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3998 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3999 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4001 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
4002 curves_duplicate_2 = bpy.context.object
4003 objects_to_delete.append(curves_duplicate_2)
4005 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
4006 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4007 curves_duplicate_2.select_set(True)
4008 bpy.context.view_layer.objects.active = curves_duplicate_2
4010 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
4011 curves_duplicate_2.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX"
4012 curves_duplicate_2.modifiers["Shrinkwrap"].target = GP_strokes_mesh
4013 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier='Shrinkwrap')
4015 # Get the distance of each vert from its original position to its position with Shrinkwrap
4016 nearest_points_coords = {}
4017 for st_idx in range(len(curves_duplicate_1.data.splines)):
4018 for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
4019 bp_1_co = curves_duplicate_1.matrix_world @ \
4020 curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
4022 bp_2_co = curves_duplicate_2.matrix_world @ \
4023 curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
4025 if bp_idx == 0:
4026 shortest_dist = (bp_1_co - bp_2_co).length
4027 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
4028 "%.4f" % bp_2_co[1],
4029 "%.4f" % bp_2_co[2])
4031 dist = (bp_1_co - bp_2_co).length
4033 if dist < shortest_dist:
4034 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
4035 "%.4f" % bp_2_co[1],
4036 "%.4f" % bp_2_co[2])
4037 shortest_dist = dist
4039 # Get all coords of GP strokes points, for comparison
4040 GP_strokes_coords = []
4041 for st_idx in range(len(GP_strokes_curve.data.splines)):
4042 GP_strokes_coords.append(
4043 [("%.4f" % x if "%.4f" % x != "-0.00" else "0.00",
4044 "%.4f" % y if "%.4f" % y != "-0.00" else "0.00",
4045 "%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for
4046 x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]]
4049 # Check the point of the GP strokes with the same coords as
4050 # the nearest points of the curves (with shrinkwrap)
4052 # Dictionary with GP stroke index as index, and a list as value.
4053 # The list has as index the point index of the GP stroke
4054 # nearest to the spline, and as value the spline index
4055 GP_connection_points = {}
4056 for gp_st_idx in range(len(GP_strokes_coords)):
4057 GPvert_spline_relationship = {}
4059 for splines_st_idx in range(len(nearest_points_coords)):
4060 if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
4061 GPvert_spline_relationship[
4062 GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])
4063 ] = splines_st_idx
4065 GP_connection_points[gp_st_idx] = GPvert_spline_relationship
4067 # Get the splines new order
4068 splines_new_order = []
4069 for i in GP_connection_points:
4070 dict_keys = sorted(GP_connection_points[i].keys()) # Sort dictionaries by key
4072 for k in dict_keys:
4073 splines_new_order.append(GP_connection_points[i][k])
4075 # Reorder
4076 curve_original_name = self.main_curve.name
4078 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4079 self.main_curve.select_set(True)
4080 bpy.context.view_layer.objects.active = self.main_curve
4082 self.main_curve.name = "SURFSKIO_CRV_ORD"
4084 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4085 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4086 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4088 for _sp_idx in range(len(self.main_curve.data.splines)):
4089 self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
4091 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4092 bpy.ops.curve.separate('EXEC_REGION_WIN')
4093 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4095 # Get the names of the separated splines objects in the original order
4096 splines_unordered = {}
4097 for o in bpy.data.objects:
4098 if o.name.find("SURFSKIO_CRV_ORD") != -1:
4099 spline_order_string = o.name.partition(".")[2]
4101 if spline_order_string != "" and int(spline_order_string) > 0:
4102 spline_order_index = int(spline_order_string) - 1
4103 splines_unordered[spline_order_index] = o.name
4105 # Join all splines objects in final order
4106 for order_idx in splines_new_order:
4107 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4108 bpy.data.objects[splines_unordered[order_idx]].select_set(True)
4109 bpy.data.objects["SURFSKIO_CRV_ORD"].select_set(True)
4110 bpy.context.view_layer.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
4112 bpy.ops.object.join('INVOKE_REGION_WIN')
4114 # Go back to the original name of the curves object.
4115 bpy.context.object.name = curve_original_name
4117 # Delete all unused objects
4118 bpy.ops.object.delete({"selected_objects": objects_to_delete})
4120 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4121 bpy.data.objects[curve_original_name].select_set(True)
4122 bpy.context.view_layer.objects.active = bpy.data.objects[curve_original_name]
4124 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4125 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4127 try:
4128 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
4129 except:
4130 pass
4133 return {"FINISHED"}
4135 def invoke(self, context, event):
4136 self.main_curve = bpy.context.object
4137 there_are_GP_strokes = False
4139 try:
4140 # Get the active grease pencil layer
4141 strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
4143 if strokes_num > 0:
4144 there_are_GP_strokes = True
4145 except:
4146 pass
4148 if there_are_GP_strokes:
4149 self.execute(context)
4150 self.report({'INFO'}, "Splines have been reordered")
4151 else:
4152 self.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4154 return {"FINISHED"}
4156 # ----------------------------
4157 # Set first points operator
4158 class CURVE_OT_SURFSK_first_points(Operator):
4159 bl_idname = "curve.surfsk_first_points"
4160 bl_label = "Bsurfaces set first points"
4161 bl_description = "Set the selected points as the first point of each spline"
4162 bl_options = {'REGISTER', 'UNDO'}
4164 def execute(self, context):
4165 splines_to_invert = []
4167 # Check non-cyclic splines to invert
4168 for i in range(len(self.main_curve.data.splines)):
4169 b_points = self.main_curve.data.splines[i].bezier_points
4171 if i not in self.cyclic_splines: # Only for non-cyclic splines
4172 if b_points[len(b_points) - 1].select_control_point:
4173 splines_to_invert.append(i)
4175 # Reorder points of cyclic splines, and set all handles to "Automatic"
4177 # Check first selected point
4178 cyclic_splines_new_first_pt = {}
4179 for i in self.cyclic_splines:
4180 sp = self.main_curve.data.splines[i]
4182 for t in range(len(sp.bezier_points)):
4183 bp = sp.bezier_points[t]
4184 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
4185 cyclic_splines_new_first_pt[i] = t
4186 break # To take only one if there are more
4188 # Reorder
4189 for spline_idx in cyclic_splines_new_first_pt:
4190 sp = self.main_curve.data.splines[spline_idx]
4192 spline_old_coords = []
4193 for bp_old in sp.bezier_points:
4194 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
4196 left_handle_type = str(bp_old.handle_left_type)
4197 left_handle_length = float(bp_old.handle_left.length)
4198 left_handle_xyz = (
4199 float(bp_old.handle_left.x),
4200 float(bp_old.handle_left.y),
4201 float(bp_old.handle_left.z)
4203 right_handle_type = str(bp_old.handle_right_type)
4204 right_handle_length = float(bp_old.handle_right.length)
4205 right_handle_xyz = (
4206 float(bp_old.handle_right.x),
4207 float(bp_old.handle_right.y),
4208 float(bp_old.handle_right.z)
4210 spline_old_coords.append(
4211 [coords, left_handle_type,
4212 right_handle_type, left_handle_length,
4213 right_handle_length, left_handle_xyz,
4214 right_handle_xyz]
4217 for t in range(len(sp.bezier_points)):
4218 bp = sp.bezier_points
4220 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
4221 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
4222 else:
4223 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
4225 bp[t].co = Vector(spline_old_coords[new_index][0])
4227 bp[t].handle_left.length = spline_old_coords[new_index][3]
4228 bp[t].handle_right.length = spline_old_coords[new_index][4]
4230 bp[t].handle_left_type = "FREE"
4231 bp[t].handle_right_type = "FREE"
4233 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
4234 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
4235 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
4237 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
4238 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
4239 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
4241 bp[t].handle_left_type = spline_old_coords[new_index][1]
4242 bp[t].handle_right_type = spline_old_coords[new_index][2]
4244 # Invert the non-cyclic splines designated above
4245 for i in range(len(splines_to_invert)):
4246 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4248 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4249 self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
4250 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4252 bpy.ops.curve.switch_direction()
4254 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4256 # Keep selected the first vert of each spline
4257 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4258 for i in range(len(self.main_curve.data.splines)):
4259 if not self.main_curve.data.splines[i].use_cyclic_u:
4260 bp = self.main_curve.data.splines[i].bezier_points[0]
4261 else:
4262 bp = self.main_curve.data.splines[i].bezier_points[
4263 len(self.main_curve.data.splines[i].bezier_points) - 1
4266 bp.select_control_point = True
4267 bp.select_right_handle = True
4268 bp.select_left_handle = True
4270 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4272 return {'FINISHED'}
4274 def invoke(self, context, event):
4275 self.main_curve = bpy.context.object
4277 # Check if all curves are Bezier, and detect which ones are cyclic
4278 self.cyclic_splines = []
4279 for i in range(len(self.main_curve.data.splines)):
4280 if self.main_curve.data.splines[i].type != "BEZIER":
4281 self.report({'WARNING'}, "All splines must be Bezier type")
4283 return {'CANCELLED'}
4284 else:
4285 if self.main_curve.data.splines[i].use_cyclic_u:
4286 self.cyclic_splines.append(i)
4288 self.execute(context)
4289 self.report({'INFO'}, "First points have been set")
4291 return {'FINISHED'}
4294 # Add-ons Preferences Update Panel
4296 # Define Panel classes for updating
4297 panels = (
4298 VIEW3D_PT_tools_SURFSK_mesh,
4299 VIEW3D_PT_tools_SURFSK_curve
4303 def conver_gpencil_to_curve(self, context, pencil, type):
4304 newCurve = bpy.data.curves.new(type + '_curve', type='CURVE')
4305 newCurve.dimensions = '3D'
4306 CurveObject = object_utils.object_data_add(context, newCurve)
4307 error = False
4309 if type == 'GPensil':
4310 try:
4311 strokes = pencil.data.layers.active.active_frame.strokes
4312 except:
4313 error = True
4314 CurveObject.location = pencil.location
4315 CurveObject.rotation_euler = pencil.rotation_euler
4316 CurveObject.scale = pencil.scale
4317 elif type == 'Annotation':
4318 try:
4319 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
4320 except:
4321 error = True
4322 CurveObject.location = (0.0, 0.0, 0.0)
4323 CurveObject.rotation_euler = (0.0, 0.0, 0.0)
4324 CurveObject.scale = (1.0, 1.0, 1.0)
4326 if not error:
4327 for i, _stroke in enumerate(strokes):
4328 stroke_points = strokes[i].points
4329 data_list = [ (point.co.x, point.co.y, point.co.z)
4330 for point in stroke_points ]
4331 points_to_add = len(data_list)-1
4333 flat_list = []
4334 for point in data_list:
4335 flat_list.extend(point)
4337 spline = newCurve.splines.new(type='BEZIER')
4338 spline.bezier_points.add(points_to_add)
4339 spline.bezier_points.foreach_set("co", flat_list)
4341 for point in spline.bezier_points:
4342 point.handle_left_type="AUTO"
4343 point.handle_right_type="AUTO"
4345 return CurveObject
4346 else:
4347 return None
4350 def update_panel(self, context):
4351 message = "Bsurfaces GPL Edition: Updating Panel locations has failed"
4352 try:
4353 for panel in panels:
4354 if "bl_rna" in panel.__dict__:
4355 bpy.utils.unregister_class(panel)
4357 for panel in panels:
4358 category = context.preferences.addons[__name__].preferences.category
4359 if category != 'Tool':
4360 panel.bl_category = context.preferences.addons[__name__].preferences.category
4361 else:
4362 context.preferences.addons[__name__].preferences.category = 'Edit'
4363 panel.bl_category = 'Edit'
4364 raise ValueError("You can not install add-ons in the Tool panel")
4365 bpy.utils.register_class(panel)
4367 except Exception as e:
4368 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
4369 pass
4371 def makeMaterial(name, diffuse):
4373 if name in bpy.data.materials:
4374 material = bpy.data.materials[name]
4375 material.diffuse_color = diffuse
4376 else:
4377 material = bpy.data.materials.new(name)
4378 material.diffuse_color = diffuse
4380 return material
4382 def update_mesh(self, context):
4383 try:
4384 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4385 bpy.ops.object.select_all(action='DESELECT')
4386 bpy.context.view_layer.update()
4387 global global_mesh_object
4388 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4389 bpy.data.objects[global_mesh_object].select_set(True)
4390 bpy.context.view_layer.objects.active = bpy.data.objects[global_mesh_object]
4391 except:
4392 print("Select mesh object")
4394 def update_gpencil(self, context):
4395 try:
4396 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4397 bpy.ops.object.select_all(action='DESELECT')
4398 bpy.context.view_layer.update()
4399 global global_gpencil_object
4400 global_gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil.name
4401 bpy.data.objects[global_gpencil_object].select_set(True)
4402 bpy.context.view_layer.objects.active = bpy.data.objects[global_gpencil_object]
4403 except:
4404 print("Select gpencil object")
4406 def update_curve(self, context):
4407 try:
4408 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4409 bpy.ops.object.select_all(action='DESELECT')
4410 bpy.context.view_layer.update()
4411 global global_curve_object
4412 global_curve_object = bpy.context.scene.bsurfaces.SURFSK_curve.name
4413 bpy.data.objects[global_curve_object].select_set(True)
4414 bpy.context.view_layer.objects.active = bpy.data.objects[global_curve_object]
4415 except:
4416 print("Select curve object")
4418 def update_color(self, context):
4419 try:
4420 global global_color
4421 global global_mesh_object
4422 material = makeMaterial("BSurfaceMesh", bpy.context.scene.bsurfaces.SURFSK_mesh_color)
4423 if bpy.data.objects[global_mesh_object].data.materials:
4424 bpy.data.objects[global_mesh_object].data.materials[0] = material
4425 else:
4426 bpy.data.objects[global_mesh_object].data.materials.append(material)
4427 diffuse_color = material.diffuse_color
4428 global_color = (diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3])
4429 except:
4430 print("Select mesh object")
4432 def update_Shrinkwrap_offset(self, context):
4433 try:
4434 global global_offset
4435 global_offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
4436 global global_mesh_object
4437 modifier = bpy.data.objects[global_mesh_object].modifiers["Shrinkwrap"]
4438 modifier.offset = global_offset
4439 except:
4440 print("Shrinkwrap modifier not found")
4442 def update_in_front(self, context):
4443 try:
4444 global global_in_front
4445 global_in_front = bpy.context.scene.bsurfaces.SURFSK_in_front
4446 global global_mesh_object
4447 bpy.data.objects[global_mesh_object].show_in_front = global_in_front
4448 except:
4449 print("Select mesh object")
4451 def update_show_wire(self, context):
4452 try:
4453 global global_show_wire
4454 global_show_wire = bpy.context.scene.bsurfaces.SURFSK_show_wire
4455 global global_mesh_object
4456 bpy.data.objects[global_mesh_object].show_wire = global_show_wire
4457 except:
4458 print("Select mesh object")
4460 def update_shade_smooth(self, context):
4461 try:
4462 global global_shade_smooth
4463 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
4465 contex_mode = bpy.context.mode
4467 if bpy.ops.object.mode_set.poll():
4468 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4470 bpy.ops.object.select_all(action='DESELECT')
4471 global global_mesh_object
4472 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4473 bpy.data.objects[global_mesh_object].select_set(True)
4475 if global_shade_smooth:
4476 bpy.ops.object.shade_smooth()
4477 else:
4478 bpy.ops.object.shade_flat()
4480 if contex_mode == "EDIT_MESH":
4481 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4483 except:
4484 print("Select mesh object")
4487 class BsurfPreferences(AddonPreferences):
4488 # this must match the addon name, use '__package__'
4489 # when defining this in a submodule of a python package.
4490 bl_idname = __name__
4492 category: StringProperty(
4493 name="Tab Category",
4494 description="Choose a name for the category of the panel",
4495 default="Edit",
4496 update=update_panel
4499 def draw(self, context):
4500 layout = self.layout
4502 row = layout.row()
4503 col = row.column()
4504 col.label(text="Tab Category:")
4505 col.prop(self, "category", text="")
4507 # Properties
4508 class BsurfacesProps(PropertyGroup):
4509 SURFSK_guide: EnumProperty(
4510 name="Guide:",
4511 items=[
4512 ('Annotation', 'Annotation', 'Annotation'),
4513 ('GPencil', 'GPencil', 'GPencil'),
4514 ('Curve', 'Curve', 'Curve')
4516 default="Annotation"
4518 SURFSK_edges_U: IntProperty(
4519 name="Cross",
4520 description="Number of face-loops crossing the strokes",
4521 default=5,
4522 min=1,
4523 max=200
4525 SURFSK_edges_V: IntProperty(
4526 name="Follow",
4527 description="Number of face-loops following the strokes",
4528 default=1,
4529 min=1,
4530 max=200
4532 SURFSK_cyclic_cross: BoolProperty(
4533 name="Cyclic Cross",
4534 description="Make cyclic the face-loops crossing the strokes",
4535 default=False
4537 SURFSK_cyclic_follow: BoolProperty(
4538 name="Cyclic Follow",
4539 description="Make cyclic the face-loops following the strokes",
4540 default=False
4542 SURFSK_keep_strokes: BoolProperty(
4543 name="Keep strokes",
4544 description="Keeps the sketched strokes or curves after adding the surface",
4545 default=False
4547 SURFSK_automatic_join: BoolProperty(
4548 name="Automatic join",
4549 description="Join automatically vertices of either surfaces "
4550 "generated by crosshatching, or from the borders of closed shapes",
4551 default=True
4553 SURFSK_loops_on_strokes: BoolProperty(
4554 name="Loops on strokes",
4555 description="Make the loops match the paths of the strokes",
4556 default=True
4558 SURFSK_precision: IntProperty(
4559 name="Precision",
4560 description="Precision level of the surface calculation",
4561 default=2,
4562 min=1,
4563 max=100
4565 SURFSK_mesh: PointerProperty(
4566 name="Mesh of BSurface",
4567 type=bpy.types.Object,
4568 description="Mesh of BSurface",
4569 update=update_mesh,
4571 SURFSK_gpencil: PointerProperty(
4572 name="GreasePencil object",
4573 type=bpy.types.Object,
4574 description="GreasePencil object",
4575 update=update_gpencil,
4577 SURFSK_curve: PointerProperty(
4578 name="Curve object",
4579 type=bpy.types.Object,
4580 description="Curve object",
4581 update=update_curve,
4583 SURFSK_mesh_color: FloatVectorProperty(
4584 name="Mesh color",
4585 default=(1.0, 0.0, 0.0, 0.3),
4586 size=4,
4587 subtype="COLOR",
4588 min=0,
4589 max=1,
4590 update=update_color,
4591 description="Mesh color",
4593 SURFSK_Shrinkwrap_offset: FloatProperty(
4594 name="Shrinkwrap offset",
4595 default=0.01,
4596 precision=3,
4597 description="Distance to keep from the target",
4598 update=update_Shrinkwrap_offset,
4600 SURFSK_in_front: BoolProperty(
4601 name="In Front",
4602 description="Make the object draw in front of others",
4603 default=False,
4604 update=update_in_front,
4606 SURFSK_show_wire: BoolProperty(
4607 name="Show wire",
4608 description="Add the object’s wireframe over solid drawing",
4609 default=False,
4610 update=update_show_wire,
4612 SURFSK_shade_smooth: BoolProperty(
4613 name="Shade smooth",
4614 description="Render and display faces smooth, using interpolated Vertex Normals",
4615 default=False,
4616 update=update_shade_smooth,
4619 classes = (
4620 MESH_OT_SURFSK_init,
4621 MESH_OT_SURFSK_add_modifiers,
4622 MESH_OT_SURFSK_add_surface,
4623 MESH_OT_SURFSK_edit_surface,
4624 GPENCIL_OT_SURFSK_add_strokes,
4625 GPENCIL_OT_SURFSK_edit_strokes,
4626 GPENCIL_OT_SURFSK_strokes_to_curves,
4627 GPENCIL_OT_SURFSK_annotation_to_curves,
4628 GPENCIL_OT_SURFSK_add_annotation,
4629 CURVE_OT_SURFSK_edit_curve,
4630 CURVE_OT_SURFSK_reorder_splines,
4631 CURVE_OT_SURFSK_first_points,
4632 BsurfPreferences,
4633 BsurfacesProps
4636 def register():
4637 for cls in classes:
4638 bpy.utils.register_class(cls)
4640 for panel in panels:
4641 bpy.utils.register_class(panel)
4643 bpy.types.Scene.bsurfaces = PointerProperty(type=BsurfacesProps)
4644 update_panel(None, bpy.context)
4646 def unregister():
4647 for panel in panels:
4648 bpy.utils.unregister_class(panel)
4650 for cls in classes:
4651 bpy.utils.unregister_class(cls)
4653 del bpy.types.Scene.bsurfaces
4655 if __name__ == "__main__":
4656 register()