sun_position: fix warning from deleted prop in User Preferences
[blender-addons.git] / mesh_bsurfaces.py
blob10125a7786ad18786e2297e9dce82eec02da0eda
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, 7),
24 "blender": (2, 80, 0),
25 "location": "View3D EditMode > Sidebar > Edit Tab",
26 "description": "Modeling and retopology tool",
27 "wiki_url": "https://docs.blender.org/manual/nb/dev/addons/"
28 "mesh/bsurfaces.html",
29 "category": "Mesh",
33 import bpy
34 import bmesh
35 from bpy_extras import object_utils
37 import operator
38 from mathutils import Matrix, Vector
39 from mathutils.geometry import (
40 intersect_line_line,
41 intersect_point_line,
43 from math import (
44 degrees,
45 pi,
46 sqrt,
48 from bpy.props import (
49 BoolProperty,
50 FloatProperty,
51 IntProperty,
52 StringProperty,
53 PointerProperty,
54 EnumProperty,
55 FloatVectorProperty,
57 from bpy.types import (
58 Operator,
59 Panel,
60 PropertyGroup,
61 AddonPreferences,
64 # ----------------------------
65 # GLOBAL
66 global_color = [1.0, 0.0, 0.0, 0.3]
67 global_offset = 0.01
68 global_in_front = False
69 global_shade_smooth = False
70 global_show_wire = True
71 global_mesh_object = ""
72 global_gpencil_object = ""
73 global_curve_object = ""
75 # ----------------------------
76 # Panels
77 class VIEW3D_PT_tools_SURFSK_mesh(Panel):
78 bl_space_type = 'VIEW_3D'
79 bl_region_type = 'UI'
80 bl_category = 'Edit'
81 bl_label = "Bsurfaces"
83 def draw(self, context):
84 layout = self.layout
85 scn = context.scene.bsurfaces
87 col = layout.column(align=True)
88 row = layout.row()
89 row.separator()
90 col.operator("mesh.surfsk_init", text="Initialize (Add BSurface mesh)")
91 col.operator("mesh.surfsk_add_modifiers", text="Add Mirror and others modifiers")
93 col.label(text="Mesh of BSurface:")
94 col.prop(scn, "SURFSK_mesh", text="")
95 col.prop(scn, "SURFSK_mesh_color")
96 col.prop(scn, "SURFSK_Shrinkwrap_offset")
97 col.prop(scn, "SURFSK_in_front")
98 col.prop(scn, "SURFSK_shade_smooth")
99 col.prop(scn, "SURFSK_show_wire")
101 col.label(text="Guide strokes:")
102 col.row().prop(scn, "SURFSK_guide", expand=True)
103 if scn.SURFSK_guide == 'GPencil':
104 col.prop(scn, "SURFSK_gpencil", text="")
105 col.separator()
106 if scn.SURFSK_guide == 'Curve':
107 col.prop(scn, "SURFSK_curve", text="")
108 col.separator()
110 col.separator()
111 col.operator("mesh.surfsk_add_surface", text="Add Surface")
112 col.operator("mesh.surfsk_edit_surface", text="Edit Surface")
114 col.separator()
115 if scn.SURFSK_guide == 'GPencil':
116 col.operator("gpencil.surfsk_add_strokes", text="Add Strokes")
117 col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
118 col.separator()
119 col.operator("gpencil.surfsk_strokes_to_curves", text="Strokes to curves")
121 if scn.SURFSK_guide == 'Annotation':
122 col.operator("gpencil.surfsk_add_annotation", text="Add Annotation")
123 col.separator()
124 col.operator("gpencil.surfsk_annotations_to_curves", text="Annotation to curves")
126 if scn.SURFSK_guide == 'Curve':
127 col.operator("curve.surfsk_edit_curve", text="Edit curve")
129 col.separator()
130 col.label(text="Initial settings:")
131 col.prop(scn, "SURFSK_edges_U")
132 col.prop(scn, "SURFSK_edges_V")
133 col.prop(scn, "SURFSK_cyclic_cross")
134 col.prop(scn, "SURFSK_cyclic_follow")
135 col.prop(scn, "SURFSK_loops_on_strokes")
136 col.prop(scn, "SURFSK_automatic_join")
137 col.prop(scn, "SURFSK_keep_strokes")
139 class VIEW3D_PT_tools_SURFSK_curve(Panel):
140 bl_space_type = 'VIEW_3D'
141 bl_region_type = 'UI'
142 bl_context = "curve_edit"
143 bl_category = 'Edit'
144 bl_label = "Bsurfaces"
146 @classmethod
147 def poll(cls, context):
148 return context.active_object
150 def draw(self, context):
151 layout = self.layout
153 col = layout.column(align=True)
154 row = layout.row()
155 row.separator()
156 col.operator("curve.surfsk_first_points", text="Set First Points")
157 col.operator("curve.switch_direction", text="Switch Direction")
158 col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
161 # ----------------------------
162 # Returns the type of strokes used
163 def get_strokes_type(context):
164 strokes_type = "NO_STROKES"
165 strokes_num = 0
167 # Check if they are annotation
168 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
169 try:
170 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
172 strokes_num = len(strokes)
174 if strokes_num > 0:
175 strokes_type = "GP_ANNOTATION"
176 except:
177 strokes_type = "NO_STROKES"
179 # Check if they are grease pencil
180 if context.scene.bsurfaces.SURFSK_guide == 'GPencil':
181 try:
182 global global_gpencil_object
183 gpencil = bpy.data.objects[global_gpencil_object]
184 strokes = gpencil.data.layers.active.active_frame.strokes
186 strokes_num = len(strokes)
188 if strokes_num > 0:
189 strokes_type = "GP_STROKES"
190 except:
191 strokes_type = "NO_STROKES"
193 # Check if they are curves, if there aren't grease pencil strokes
194 if context.scene.bsurfaces.SURFSK_guide == 'Curve':
195 try:
196 global global_curve_object
197 ob = bpy.data.objects[global_curve_object]
198 if ob.type == "CURVE":
199 strokes_type = "EXTERNAL_CURVE"
200 strokes_num = len(ob.data.splines)
202 # Check if there is any non-bezier spline
203 for i in range(len(ob.data.splines)):
204 if ob.data.splines[i].type != "BEZIER":
205 strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
206 break
208 else:
209 strokes_type = "EXTERNAL_NO_CURVE"
210 except:
211 strokes_type = "NO_STROKES"
213 # Check if they are mesh
214 try:
215 global global_mesh_object
216 self.main_object = bpy.data.objects[global_mesh_object]
217 total_vert_sel = len([v for v in self.main_object.data.vertices if v.select])
219 # Check if there is a single stroke without any selection in the object
220 if strokes_num == 1 and total_vert_sel == 0:
221 if strokes_type == "EXTERNAL_CURVE":
222 strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
223 elif strokes_type == "GP_STROKES":
224 strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"
226 if strokes_num == 0 and total_vert_sel > 0:
227 strokes_type = "SELECTION_ALONE"
228 except:
229 pass
231 return strokes_type
233 # ----------------------------
234 # Surface generator operator
235 class MESH_OT_SURFSK_add_surface(Operator):
236 bl_idname = "mesh.surfsk_add_surface"
237 bl_label = "Bsurfaces add surface"
238 bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
239 bl_options = {'REGISTER', 'UNDO'}
241 is_fill_faces: BoolProperty(
242 default=False
244 selection_U_exists: BoolProperty(
245 default=False
247 selection_V_exists: BoolProperty(
248 default=False
250 selection_U2_exists: BoolProperty(
251 default=False
253 selection_V2_exists: BoolProperty(
254 default=False
256 selection_V_is_closed: BoolProperty(
257 default=False
259 selection_U_is_closed: BoolProperty(
260 default=False
262 selection_V2_is_closed: BoolProperty(
263 default=False
265 selection_U2_is_closed: BoolProperty(
266 default=False
269 edges_U: IntProperty(
270 name="Cross",
271 description="Number of face-loops crossing the strokes",
272 default=1,
273 min=1,
274 max=200
276 edges_V: IntProperty(
277 name="Follow",
278 description="Number of face-loops following the strokes",
279 default=1,
280 min=1,
281 max=200
283 cyclic_cross: BoolProperty(
284 name="Cyclic Cross",
285 description="Make cyclic the face-loops crossing the strokes",
286 default=False
288 cyclic_follow: BoolProperty(
289 name="Cyclic Follow",
290 description="Make cyclic the face-loops following the strokes",
291 default=False
293 loops_on_strokes: BoolProperty(
294 name="Loops on strokes",
295 description="Make the loops match the paths of the strokes",
296 default=False
298 automatic_join: BoolProperty(
299 name="Automatic join",
300 description="Join automatically vertices of either surfaces generated "
301 "by crosshatching, or from the borders of closed shapes",
302 default=False
304 join_stretch_factor: FloatProperty(
305 name="Stretch",
306 description="Amount of stretching or shrinking allowed for "
307 "edges when joining vertices automatically",
308 default=1,
309 min=0,
310 max=3,
311 subtype='FACTOR'
313 keep_strokes: BoolProperty(
314 name="Keep strokes",
315 description="Keeps the sketched strokes or curves after adding the surface",
316 default=False
318 strokes_type: StringProperty()
319 initial_global_undo_state: BoolProperty()
322 def draw(self, context):
323 layout = self.layout
324 col = layout.column(align=True)
325 row = layout.row()
327 if not self.is_fill_faces:
328 row.separator()
329 if not self.is_crosshatch:
330 if not self.selection_U_exists:
331 col.prop(self, "edges_U")
332 row.separator()
334 if not self.selection_V_exists:
335 col.prop(self, "edges_V")
336 row.separator()
338 row.separator()
340 if not self.selection_U_exists:
341 if not (
342 (self.selection_V_exists and not self.selection_V_is_closed) or
343 (self.selection_V2_exists and not self.selection_V2_is_closed)
345 col.prop(self, "cyclic_cross")
347 if not self.selection_V_exists:
348 if not (
349 (self.selection_U_exists and not self.selection_U_is_closed) or
350 (self.selection_U2_exists and not self.selection_U2_is_closed)
352 col.prop(self, "cyclic_follow")
354 col.prop(self, "loops_on_strokes")
356 col.prop(self, "automatic_join")
358 if self.automatic_join:
359 row.separator()
360 col.separator()
361 row.separator()
362 col.prop(self, "join_stretch_factor")
364 col.prop(self, "keep_strokes")
366 # Get an ordered list of a chain of vertices
367 def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx,
368 first_vert_idx, middle_vertex_idx, closing_vert_idx):
369 # Order selected vertices.
370 verts_ordered = []
371 if closing_vert_idx is not None:
372 verts_ordered.append(ob.data.vertices[closing_vert_idx])
374 verts_ordered.append(ob.data.vertices[first_vert_idx])
375 prev_v = first_vert_idx
376 prev_ed = None
377 finish_while = False
378 while True:
379 edges_non_matched = 0
380 for i in all_selected_edges_idx:
381 if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and \
382 ob.data.edges[i].vertices[1] in all_selected_verts_idx:
384 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
385 prev_v = ob.data.edges[i].vertices[1]
386 prev_ed = ob.data.edges[i]
387 elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and \
388 ob.data.edges[i].vertices[0] in all_selected_verts_idx:
390 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
391 prev_v = ob.data.edges[i].vertices[0]
392 prev_ed = ob.data.edges[i]
393 else:
394 edges_non_matched += 1
396 if edges_non_matched == len(all_selected_edges_idx):
397 finish_while = True
399 if finish_while:
400 break
402 if closing_vert_idx is not None:
403 verts_ordered.append(ob.data.vertices[closing_vert_idx])
405 if middle_vertex_idx is not None:
406 verts_ordered.append(ob.data.vertices[middle_vertex_idx])
407 verts_ordered.reverse()
409 return tuple(verts_ordered)
411 # Calculates length of a chain of points.
412 def get_chain_length(self, object, verts_ordered):
413 matrix = object.matrix_world
415 edges_lengths = []
416 edges_lengths_sum = 0
417 for i in range(0, len(verts_ordered)):
418 if i == 0:
419 prev_v_co = matrix @ verts_ordered[i].co
420 else:
421 v_co = matrix @ verts_ordered[i].co
423 v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
424 edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
426 edges_lengths.append(edge_length)
427 edges_lengths_sum += edge_length
429 prev_v_co = v_co
431 return edges_lengths, edges_lengths_sum
433 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
434 def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
435 edges_proportions = []
436 if use_boundaries:
437 verts_count = 1
438 for l in edges_lengths:
439 edges_proportions.append(l / edges_lengths_sum)
440 verts_count += 1
441 else:
442 verts_count = 1
443 for _n in range(0, fixed_edges_num):
444 edges_proportions.append(1 / fixed_edges_num)
445 verts_count += 1
447 return edges_proportions
449 # Calculates the angle between two pairs of points in space
450 def orientation_difference(self, points_A_co, points_B_co):
451 # each parameter should be a list with two elements,
452 # and each element should be a x,y,z coordinate
453 vec_A = points_A_co[0] - points_A_co[1]
454 vec_B = points_B_co[0] - points_B_co[1]
456 angle = vec_A.angle(vec_B)
458 if angle > 0.5 * pi:
459 angle = abs(angle - pi)
461 return angle
463 # Calculate the which vert of verts_idx list is the nearest one
464 # to the point_co coordinates, and the distance
465 def shortest_distance(self, object, point_co, verts_idx):
466 matrix = object.matrix_world
468 for i in range(0, len(verts_idx)):
469 dist = (point_co - matrix @ object.data.vertices[verts_idx[i]].co).length
470 if i == 0:
471 prev_dist = dist
472 nearest_vert_idx = verts_idx[i]
473 shortest_dist = dist
475 if dist < prev_dist:
476 prev_dist = dist
477 nearest_vert_idx = verts_idx[i]
478 shortest_dist = dist
480 return nearest_vert_idx, shortest_dist
482 # Returns the index of the opposite vert tip in a chain, given a vert tip index
483 # as parameter, and a multidimentional list with all pairs of tips
484 def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
485 opposite_vert_tip_idx = None
486 for i in range(0, len(all_chains_tips_idx)):
487 if vert_tip_idx == all_chains_tips_idx[i][0]:
488 opposite_vert_tip_idx = all_chains_tips_idx[i][1]
489 if vert_tip_idx == all_chains_tips_idx[i][1]:
490 opposite_vert_tip_idx = all_chains_tips_idx[i][0]
492 return opposite_vert_tip_idx
494 # Simplifies a spline and returns the new points coordinates
495 def simplify_spline(self, spline_coords, segments_num):
496 simplified_spline = []
497 points_between_segments = round(len(spline_coords) / segments_num)
499 simplified_spline.append(spline_coords[0])
500 for i in range(1, segments_num):
501 simplified_spline.append(spline_coords[i * points_between_segments])
503 simplified_spline.append(spline_coords[len(spline_coords) - 1])
505 return simplified_spline
507 # Returns a list with the coords of the points distributed over the splines
508 # passed to this method according to the proportions parameter
509 def distribute_pts(self, surface_splines, proportions):
511 # Calculate the length of each final surface spline
512 surface_splines_lengths = []
513 surface_splines_parsed = []
515 for sp_idx in range(0, len(surface_splines)):
516 # Calculate spline length
517 surface_splines_lengths.append(0)
519 for i in range(0, len(surface_splines[sp_idx].bezier_points)):
520 if i == 0:
521 prev_p = surface_splines[sp_idx].bezier_points[i]
522 else:
523 p = surface_splines[sp_idx].bezier_points[i]
524 edge_length = (prev_p.co - p.co).length
525 surface_splines_lengths[sp_idx] += edge_length
527 prev_p = p
529 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
530 for sp_idx in range(0, len(surface_splines)):
531 surface_splines_parsed.append([])
532 surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
534 prev_p_co = surface_splines[sp_idx].bezier_points[0].co
535 p_idx = 0
537 for prop_idx in range(len(proportions) - 1):
538 target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
539 partial_segment_length = 0
540 finish_while = False
542 while True:
543 # if not it'll pass the p_idx as an index below and crash
544 if p_idx < len(surface_splines[sp_idx].bezier_points):
545 p_co = surface_splines[sp_idx].bezier_points[p_idx].co
546 new_dist = (prev_p_co - p_co).length
548 # The new distance that could have the partial segment if
549 # it is still shorter than the target length
550 potential_segment_length = partial_segment_length + new_dist
552 # If the potential is still shorter, keep adding
553 if potential_segment_length < target_length:
554 partial_segment_length = potential_segment_length
556 p_idx += 1
557 prev_p_co = p_co
559 # If the potential is longer than the target, calculate the target
560 # (a point between the last two points), and assign
561 elif potential_segment_length > target_length:
562 remaining_dist = target_length - partial_segment_length
563 vec = p_co - prev_p_co
564 vec.normalize()
565 intermediate_co = prev_p_co + (vec * remaining_dist)
567 surface_splines_parsed[sp_idx].append(intermediate_co)
569 partial_segment_length += remaining_dist
570 prev_p_co = intermediate_co
572 finish_while = True
574 # If the potential is equal to the target, assign
575 elif potential_segment_length == target_length:
576 surface_splines_parsed[sp_idx].append(p_co)
577 prev_p_co = p_co
579 finish_while = True
581 if finish_while:
582 break
584 # last point of the spline
585 surface_splines_parsed[sp_idx].append(
586 surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
589 return surface_splines_parsed
591 # Counts the number of faces that belong to each edge
592 def edge_face_count(self, ob):
593 ed_keys_count_dict = {}
595 for face in ob.data.polygons:
596 for ed_keys in face.edge_keys:
597 if ed_keys not in ed_keys_count_dict:
598 ed_keys_count_dict[ed_keys] = 1
599 else:
600 ed_keys_count_dict[ed_keys] += 1
602 edge_face_count = []
603 for i in range(len(ob.data.edges)):
604 edge_face_count.append(0)
606 for i in range(len(ob.data.edges)):
607 ed = ob.data.edges[i]
609 v1 = ed.vertices[0]
610 v2 = ed.vertices[1]
612 if (v1, v2) in ed_keys_count_dict:
613 edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
614 elif (v2, v1) in ed_keys_count_dict:
615 edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
617 return edge_face_count
619 # Fills with faces all the selected vertices which form empty triangles or quads
620 def fill_with_faces(self, object):
621 all_selected_verts_count = self.main_object_selected_verts_count
623 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
625 # Calculate average length of selected edges
626 all_selected_verts = []
627 original_sel_edges_count = 0
628 for ed in object.data.edges:
629 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
630 coords = []
631 coords.append(object.data.vertices[ed.vertices[0]].co)
632 coords.append(object.data.vertices[ed.vertices[1]].co)
634 original_sel_edges_count += 1
636 if not ed.vertices[0] in all_selected_verts:
637 all_selected_verts.append(ed.vertices[0])
639 if not ed.vertices[1] in all_selected_verts:
640 all_selected_verts.append(ed.vertices[1])
642 tuple(all_selected_verts)
644 # Check if there is any edge selected. If not, interrupt the script
645 if original_sel_edges_count == 0 and all_selected_verts_count > 0:
646 return 0
648 # Get all edges connected to selected verts
649 all_edges_around_sel_verts = []
650 edges_connected_to_sel_verts = {}
651 verts_connected_to_every_vert = {}
652 for ed_idx in range(len(object.data.edges)):
653 ed = object.data.edges[ed_idx]
654 include_edge = False
656 if ed.vertices[0] in all_selected_verts:
657 if not ed.vertices[0] in edges_connected_to_sel_verts:
658 edges_connected_to_sel_verts[ed.vertices[0]] = []
660 edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
661 include_edge = True
663 if ed.vertices[1] in all_selected_verts:
664 if not ed.vertices[1] in edges_connected_to_sel_verts:
665 edges_connected_to_sel_verts[ed.vertices[1]] = []
667 edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
668 include_edge = True
670 if include_edge is True:
671 all_edges_around_sel_verts.append(ed_idx)
673 # Get all connected verts to each vert
674 if not ed.vertices[0] in verts_connected_to_every_vert:
675 verts_connected_to_every_vert[ed.vertices[0]] = []
677 if not ed.vertices[1] in verts_connected_to_every_vert:
678 verts_connected_to_every_vert[ed.vertices[1]] = []
680 verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
681 verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
683 # Get all verts connected to faces
684 all_verts_part_of_faces = []
685 all_edges_faces_count = []
686 all_edges_faces_count += self.edge_face_count(object)
688 # Get only the selected edges that have faces attached.
689 count_faces_of_edges_around_sel_verts = {}
690 selected_verts_with_faces = []
691 for ed_idx in all_edges_around_sel_verts:
692 count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
694 if all_edges_faces_count[ed_idx] > 0:
695 ed = object.data.edges[ed_idx]
697 if not ed.vertices[0] in selected_verts_with_faces:
698 selected_verts_with_faces.append(ed.vertices[0])
700 if not ed.vertices[1] in selected_verts_with_faces:
701 selected_verts_with_faces.append(ed.vertices[1])
703 all_verts_part_of_faces.append(ed.vertices[0])
704 all_verts_part_of_faces.append(ed.vertices[1])
706 tuple(selected_verts_with_faces)
708 # Discard unneeded verts from calculations
709 participating_verts = []
710 movable_verts = []
711 for v_idx in all_selected_verts:
712 vert_has_edges_with_one_face = False
714 # Check if the actual vert has at least one edge connected to only one face
715 for ed_idx in edges_connected_to_sel_verts[v_idx]:
716 if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
717 vert_has_edges_with_one_face = True
719 # If the vert has two or less edges connected and the vert is not part of any face.
720 # Or the vert is part of any face and at least one of
721 # the connected edges has only one face attached to it.
722 if (len(edges_connected_to_sel_verts[v_idx]) == 2 and
723 v_idx not in all_verts_part_of_faces) or \
724 len(edges_connected_to_sel_verts[v_idx]) == 1 or \
725 (v_idx in all_verts_part_of_faces and
726 vert_has_edges_with_one_face):
728 participating_verts.append(v_idx)
730 if v_idx not in all_verts_part_of_faces:
731 movable_verts.append(v_idx)
733 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
734 for mv_idx in movable_verts:
735 freeze_vert = False
736 mv_connected_verts = verts_connected_to_every_vert[mv_idx]
738 for actual_v_idx in all_selected_verts:
739 count_shared_neighbors = 0
740 checked_verts = []
742 for mv_conn_v_idx in mv_connected_verts:
743 if mv_idx != actual_v_idx:
744 if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and \
745 mv_conn_v_idx not in checked_verts:
746 count_shared_neighbors += 1
747 checked_verts.append(mv_conn_v_idx)
749 if actual_v_idx in mv_connected_verts:
750 freeze_vert = True
751 break
753 if count_shared_neighbors == 2:
754 freeze_vert = True
755 break
757 if freeze_vert:
758 break
760 if freeze_vert:
761 movable_verts.remove(mv_idx)
763 # Calculate merge distance for participating verts
764 shortest_edge_length = None
765 for ed in object.data.edges:
766 if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
767 v1 = object.data.vertices[ed.vertices[0]]
768 v2 = object.data.vertices[ed.vertices[1]]
770 length = (v1.co - v2.co).length
772 if shortest_edge_length is None:
773 shortest_edge_length = length
774 else:
775 if length < shortest_edge_length:
776 shortest_edge_length = length
778 if shortest_edge_length is not None:
779 edges_merge_distance = shortest_edge_length * 0.5
780 else:
781 edges_merge_distance = 0
783 # Get together the verts near enough. They will be merged later
784 remaining_verts = []
785 remaining_verts += participating_verts
786 for v1_idx in participating_verts:
787 if v1_idx in remaining_verts and v1_idx in movable_verts:
788 verts_to_merge = []
789 coords_verts_to_merge = {}
791 verts_to_merge.append(v1_idx)
793 v1_co = object.data.vertices[v1_idx].co
794 coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
796 for v2_idx in remaining_verts:
797 if v1_idx != v2_idx:
798 v2_co = object.data.vertices[v2_idx].co
800 dist = (v1_co - v2_co).length
802 if dist <= edges_merge_distance: # Add the verts which are near enough
803 verts_to_merge.append(v2_idx)
805 coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
807 for vm_idx in verts_to_merge:
808 remaining_verts.remove(vm_idx)
810 if len(verts_to_merge) > 1:
811 # Calculate middle point of the verts to merge.
812 sum_x_co = 0
813 sum_y_co = 0
814 sum_z_co = 0
815 movable_verts_to_merge_count = 0
816 for i in range(len(verts_to_merge)):
817 if verts_to_merge[i] in movable_verts:
818 v_co = object.data.vertices[verts_to_merge[i]].co
820 sum_x_co += v_co[0]
821 sum_y_co += v_co[1]
822 sum_z_co += v_co[2]
824 movable_verts_to_merge_count += 1
826 middle_point_co = [
827 sum_x_co / movable_verts_to_merge_count,
828 sum_y_co / movable_verts_to_merge_count,
829 sum_z_co / movable_verts_to_merge_count
832 # Check if any vert to be merged is not movable
833 shortest_dist = None
834 are_verts_not_movable = False
835 verts_not_movable = []
836 for v_merge_idx in verts_to_merge:
837 if v_merge_idx in participating_verts and v_merge_idx not in movable_verts:
838 are_verts_not_movable = True
839 verts_not_movable.append(v_merge_idx)
841 if are_verts_not_movable:
842 # Get the vert connected to faces, that is nearest to
843 # the middle point of the movable verts
844 shortest_dist = None
845 for vcf_idx in verts_not_movable:
846 dist = abs((object.data.vertices[vcf_idx].co -
847 Vector(middle_point_co)).length)
849 if shortest_dist is None:
850 shortest_dist = dist
851 nearest_vert_idx = vcf_idx
852 else:
853 if dist < shortest_dist:
854 shortest_dist = dist
855 nearest_vert_idx = vcf_idx
857 coords = object.data.vertices[nearest_vert_idx].co
858 target_point_co = [coords[0], coords[1], coords[2]]
859 else:
860 target_point_co = middle_point_co
862 # Move verts to merge to the middle position
863 for v_merge_idx in verts_to_merge:
864 if v_merge_idx in movable_verts: # Only move the verts that are not part of faces
865 object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
866 object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
867 object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
869 # Perform "Remove Doubles" to weld all the disconnected verts
870 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
871 bpy.ops.mesh.remove_doubles(threshold=0.0001)
873 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
875 # Get all the definitive selected edges, after weldding
876 selected_edges = []
877 edges_per_vert = {} # Number of faces of each selected edge
878 for ed in object.data.edges:
879 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
880 selected_edges.append(ed.index)
882 # Save all the edges that belong to each vertex.
883 if not ed.vertices[0] in edges_per_vert:
884 edges_per_vert[ed.vertices[0]] = []
886 if not ed.vertices[1] in edges_per_vert:
887 edges_per_vert[ed.vertices[1]] = []
889 edges_per_vert[ed.vertices[0]].append(ed.index)
890 edges_per_vert[ed.vertices[1]].append(ed.index)
892 # Check if all the edges connected to each vert have two faces attached to them.
893 # To discard them later and make calculations faster
894 a = []
895 a += self.edge_face_count(object)
896 tuple(a)
897 verts_surrounded_by_faces = {}
898 for v_idx in edges_per_vert:
899 edges_with_two_faces_count = 0
901 for ed_idx in edges_per_vert[v_idx]:
902 if a[ed_idx] == 2:
903 edges_with_two_faces_count += 1
905 if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
906 verts_surrounded_by_faces[v_idx] = True
907 else:
908 verts_surrounded_by_faces[v_idx] = False
910 # Get all the selected vertices
911 selected_verts_idx = []
912 for v in object.data.vertices:
913 if v.select:
914 selected_verts_idx.append(v.index)
916 # Get all the faces of the object
917 all_object_faces_verts_idx = []
918 for face in object.data.polygons:
919 face_verts = []
920 face_verts.append(face.vertices[0])
921 face_verts.append(face.vertices[1])
922 face_verts.append(face.vertices[2])
924 if len(face.vertices) == 4:
925 face_verts.append(face.vertices[3])
927 all_object_faces_verts_idx.append(face_verts)
929 # Deselect all vertices
930 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
931 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
932 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
934 # Make a dictionary with the verts related to each vert
935 related_key_verts = {}
936 for ed_idx in selected_edges:
937 ed = object.data.edges[ed_idx]
939 if not verts_surrounded_by_faces[ed.vertices[0]]:
940 if not ed.vertices[0] in related_key_verts:
941 related_key_verts[ed.vertices[0]] = []
943 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
944 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
946 if not verts_surrounded_by_faces[ed.vertices[1]]:
947 if not ed.vertices[1] in related_key_verts:
948 related_key_verts[ed.vertices[1]] = []
950 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
951 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
953 # Get groups of verts forming each face
954 faces_verts_idx = []
955 for v1 in related_key_verts: # verts-1 ....
956 for v2 in related_key_verts: # verts-2
957 if v1 != v2:
958 related_verts_in_common = []
959 v2_in_rel_v1 = False
960 v1_in_rel_v2 = False
961 for rel_v1 in related_key_verts[v1]:
962 # Check if related verts of verts-1 are related verts of verts-2
963 if rel_v1 in related_key_verts[v2]:
964 related_verts_in_common.append(rel_v1)
966 if v2 in related_key_verts[v1]:
967 v2_in_rel_v1 = True
969 if v1 in related_key_verts[v2]:
970 v1_in_rel_v2 = True
972 repeated_face = False
973 # If two verts have two related verts in common, they form a quad
974 if len(related_verts_in_common) == 2:
975 # Check if the face is already saved
976 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
978 for f_verts in all_faces_to_check_idx:
979 repeated_verts = 0
981 if len(f_verts) == 4:
982 if v1 in f_verts:
983 repeated_verts += 1
984 if v2 in f_verts:
985 repeated_verts += 1
986 if related_verts_in_common[0] in f_verts:
987 repeated_verts += 1
988 if related_verts_in_common[1] in f_verts:
989 repeated_verts += 1
991 if repeated_verts == len(f_verts):
992 repeated_face = True
993 break
995 if not repeated_face:
996 faces_verts_idx.append(
997 [v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
1000 # If Two verts have one related vert in common and
1001 # they are related to each other, they form a triangle
1002 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1003 # Check if the face is already saved.
1004 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1006 for f_verts in all_faces_to_check_idx:
1007 repeated_verts = 0
1009 if len(f_verts) == 3:
1010 if v1 in f_verts:
1011 repeated_verts += 1
1012 if v2 in f_verts:
1013 repeated_verts += 1
1014 if related_verts_in_common[0] in f_verts:
1015 repeated_verts += 1
1017 if repeated_verts == len(f_verts):
1018 repeated_face = True
1019 break
1021 if not repeated_face:
1022 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1024 # Keep only the faces that don't overlap by ignoring quads
1025 # that overlap with two adjacent triangles
1026 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1027 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1028 for i in range(len(faces_verts_idx)):
1029 for t in range(len(all_faces_to_check_idx)):
1030 if i != t:
1031 verts_in_common = 0
1033 if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
1034 for v_idx in all_faces_to_check_idx[t]:
1035 if v_idx in faces_verts_idx[i]:
1036 verts_in_common += 1
1037 # If it doesn't have all it's vertices repeated in the other face
1038 if verts_in_common == 3:
1039 if i not in faces_to_not_include_idx:
1040 faces_to_not_include_idx.append(i)
1042 # Build faces discarding the ones in faces_to_not_include
1043 me = object.data
1044 bm = bmesh.new()
1045 bm.from_mesh(me)
1047 num_faces_created = 0
1048 for i in range(len(faces_verts_idx)):
1049 if i not in faces_to_not_include_idx:
1050 bm.faces.new([bm.verts[v] for v in faces_verts_idx[i]])
1052 num_faces_created += 1
1054 bm.to_mesh(me)
1055 bm.free()
1057 for v_idx in selected_verts_idx:
1058 self.main_object.data.vertices[v_idx].select = True
1060 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
1061 bpy.ops.mesh.normals_make_consistent(inside=False)
1062 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
1064 self.update()
1066 return num_faces_created
1068 # Crosshatch skinning
1069 def crosshatch_surface_invoke(self, ob_original_splines):
1070 self.is_crosshatch = False
1071 self.crosshatch_merge_distance = 0
1073 objects_to_delete = [] # duplicated strokes to be deleted.
1075 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1076 # (without this the surface verts merging with the main object doesn't work well)
1077 self.modifiers_prev_viewport_state = []
1078 if len(self.main_object.modifiers) > 0:
1079 for m_idx in range(len(self.main_object.modifiers)):
1080 self.modifiers_prev_viewport_state.append(
1081 self.main_object.modifiers[m_idx].show_viewport
1083 self.main_object.modifiers[m_idx].show_viewport = False
1085 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1086 ob_original_splines.select_set(True)
1087 bpy.context.view_layer.objects.active = ob_original_splines
1089 if len(ob_original_splines.data.splines) >= 2:
1090 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1091 ob_splines = bpy.context.object
1092 ob_splines.name = "SURFSKIO_NE_STR"
1094 # Get estimative merge distance (sum up the distances from the first point to
1095 # all other points, then average them and then divide them)
1096 first_point_dist_sum = 0
1097 first_dist = 0
1098 second_dist = 0
1099 coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
1100 for i in range(len(ob_splines.data.splines)):
1101 sp = ob_splines.data.splines[i]
1103 if coords_first_pt != sp.bezier_points[0].co:
1104 first_dist = (coords_first_pt - sp.bezier_points[0].co).length
1106 if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
1107 second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
1109 first_point_dist_sum += first_dist + second_dist
1111 if i == 0:
1112 if first_dist != 0:
1113 shortest_dist = first_dist
1114 elif second_dist != 0:
1115 shortest_dist = second_dist
1117 if shortest_dist > first_dist and first_dist != 0:
1118 shortest_dist = first_dist
1120 if shortest_dist > second_dist and second_dist != 0:
1121 shortest_dist = second_dist
1123 self.crosshatch_merge_distance = shortest_dist / 20
1125 # Recalculation of merge distance
1127 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1129 ob_calc_merge_dist = bpy.context.object
1130 ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
1132 objects_to_delete.append(ob_calc_merge_dist)
1134 # Smooth out strokes a little to improve crosshatch detection
1135 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1136 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
1138 for i in range(4):
1139 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1141 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1142 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1144 # Convert curves into mesh
1145 ob_calc_merge_dist.data.resolution_u = 12
1146 bpy.ops.object.convert(target='MESH', keep_original=False)
1148 # Find "intersection-nodes"
1149 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1150 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1151 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1152 threshold=self.crosshatch_merge_distance)
1153 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1154 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1156 # Remove verts with less than three edges
1157 verts_edges_count = {}
1158 for ed in ob_calc_merge_dist.data.edges:
1159 v = ed.vertices
1161 if v[0] not in verts_edges_count:
1162 verts_edges_count[v[0]] = 0
1164 if v[1] not in verts_edges_count:
1165 verts_edges_count[v[1]] = 0
1167 verts_edges_count[v[0]] += 1
1168 verts_edges_count[v[1]] += 1
1170 nodes_verts_coords = []
1171 for v_idx in verts_edges_count:
1172 v = ob_calc_merge_dist.data.vertices[v_idx]
1174 if verts_edges_count[v_idx] < 3:
1175 v.select = True
1177 # Remove them
1178 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1179 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
1180 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1182 # Remove doubles to discard very near verts from calculations of distance
1183 bpy.ops.mesh.remove_doubles(
1184 'INVOKE_REGION_WIN',
1185 threshold=self.crosshatch_merge_distance * 4.0
1187 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1189 # Get all coords of the resulting nodes
1190 nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for
1191 v in ob_calc_merge_dist.data.vertices]
1193 # Check if the strokes are a crosshatch
1194 if len(nodes_verts_coords) >= 3:
1195 self.is_crosshatch = True
1197 shortest_dist = None
1198 for co_1 in nodes_verts_coords:
1199 for co_2 in nodes_verts_coords:
1200 if co_1 != co_2:
1201 dist = (Vector(co_1) - Vector(co_2)).length
1203 if shortest_dist is not None:
1204 if dist < shortest_dist:
1205 shortest_dist = dist
1206 else:
1207 shortest_dist = dist
1209 self.crosshatch_merge_distance = shortest_dist / 3
1211 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1212 ob_splines.select_set(True)
1213 bpy.context.view_layer.objects.active = ob_splines
1215 # Deselect all points
1216 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1217 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1218 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1220 # Smooth splines in a localized way, to eliminate "saw-teeth"
1221 # like shapes when there are many points
1222 for sp in ob_splines.data.splines:
1223 angle_sum = 0
1225 angle_limit = 2 # Degrees
1226 for t in range(len(sp.bezier_points)):
1227 # Because on each iteration it checks the "next two points"
1228 # of the actual. This way it doesn't go out of range
1229 if t <= len(sp.bezier_points) - 3:
1230 p1 = sp.bezier_points[t]
1231 p2 = sp.bezier_points[t + 1]
1232 p3 = sp.bezier_points[t + 2]
1234 vec_1 = p1.co - p2.co
1235 vec_2 = p2.co - p3.co
1237 if p2.co != p1.co and p2.co != p3.co:
1238 angle = vec_1.angle(vec_2)
1239 angle_sum += degrees(angle)
1241 if angle_sum >= angle_limit: # If sum of angles is grater than the limit
1242 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1243 p1.select_control_point = True
1244 p1.select_left_handle = True
1245 p1.select_right_handle = True
1247 p2.select_control_point = True
1248 p2.select_left_handle = True
1249 p2.select_right_handle = True
1251 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1252 p3.select_control_point = True
1253 p3.select_left_handle = True
1254 p3.select_right_handle = True
1256 angle_sum = 0
1258 sp.bezier_points[0].select_control_point = False
1259 sp.bezier_points[0].select_left_handle = False
1260 sp.bezier_points[0].select_right_handle = False
1262 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
1263 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
1264 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
1266 # Smooth out strokes a little to improve crosshatch detection
1267 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1269 for i in range(15):
1270 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1272 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1273 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1275 # Simplify the splines
1276 for sp in ob_splines.data.splines:
1277 angle_sum = 0
1279 sp.bezier_points[0].select_control_point = True
1280 sp.bezier_points[0].select_left_handle = True
1281 sp.bezier_points[0].select_right_handle = True
1283 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
1284 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
1285 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
1287 angle_limit = 15 # Degrees
1288 for t in range(len(sp.bezier_points)):
1289 # Because on each iteration it checks the "next two points"
1290 # of the actual. This way it doesn't go out of range
1291 if t <= len(sp.bezier_points) - 3:
1292 p1 = sp.bezier_points[t]
1293 p2 = sp.bezier_points[t + 1]
1294 p3 = sp.bezier_points[t + 2]
1296 vec_1 = p1.co - p2.co
1297 vec_2 = p2.co - p3.co
1299 if p2.co != p1.co and p2.co != p3.co:
1300 angle = vec_1.angle(vec_2)
1301 angle_sum += degrees(angle)
1302 # If sum of angles is grater than the limit
1303 if angle_sum >= angle_limit:
1304 p1.select_control_point = True
1305 p1.select_left_handle = True
1306 p1.select_right_handle = True
1308 p2.select_control_point = True
1309 p2.select_left_handle = True
1310 p2.select_right_handle = True
1312 p3.select_control_point = True
1313 p3.select_left_handle = True
1314 p3.select_right_handle = True
1316 angle_sum = 0
1318 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1319 bpy.ops.curve.select_all(action='INVERT')
1321 bpy.ops.curve.delete(type='VERT')
1322 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1324 objects_to_delete.append(ob_splines)
1326 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1327 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1328 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1330 # Check if the strokes are a crosshatch
1331 if self.is_crosshatch:
1332 all_points_coords = []
1333 for i in range(len(ob_splines.data.splines)):
1334 all_points_coords.append([])
1336 all_points_coords[i] = [Vector((x, y, z)) for
1337 x, y, z in [bp.co for
1338 bp in ob_splines.data.splines[i].bezier_points]]
1340 all_intersections = []
1341 checked_splines = []
1342 for i in range(len(all_points_coords)):
1344 for t in range(len(all_points_coords[i]) - 1):
1345 bp1_co = all_points_coords[i][t]
1346 bp2_co = all_points_coords[i][t + 1]
1348 for i2 in range(len(all_points_coords)):
1349 if i != i2 and i2 not in checked_splines:
1350 for t2 in range(len(all_points_coords[i2]) - 1):
1351 bp3_co = all_points_coords[i2][t2]
1352 bp4_co = all_points_coords[i2][t2 + 1]
1354 intersec_coords = intersect_line_line(
1355 bp1_co, bp2_co, bp3_co, bp4_co
1357 if intersec_coords is not None:
1358 dist = (intersec_coords[0] - intersec_coords[1]).length
1360 if dist <= self.crosshatch_merge_distance * 1.5:
1361 _temp_co, percent1 = intersect_point_line(
1362 intersec_coords[0], bp1_co, bp2_co
1364 if (percent1 >= -0.02 and percent1 <= 1.02):
1365 _temp_co, percent2 = intersect_point_line(
1366 intersec_coords[1], bp3_co, bp4_co
1368 if (percent2 >= -0.02 and percent2 <= 1.02):
1369 # Format: spline index, first point index from
1370 # corresponding segment, percentage from first point of
1371 # actual segment, coords of intersection point
1372 all_intersections.append(
1373 (i, t, percent1,
1374 ob_splines.matrix_world @ intersec_coords[0])
1376 all_intersections.append(
1377 (i2, t2, percent2,
1378 ob_splines.matrix_world @ intersec_coords[1])
1381 checked_splines.append(i)
1382 # Sort list by spline, then by corresponding first point index of segment,
1383 # and then by percentage from first point of segment: elements 0 and 1 respectively
1384 all_intersections.sort(key=operator.itemgetter(0, 1, 2))
1386 self.crosshatch_strokes_coords = {}
1387 for i in range(len(all_intersections)):
1388 if not all_intersections[i][0] in self.crosshatch_strokes_coords:
1389 self.crosshatch_strokes_coords[all_intersections[i][0]] = []
1391 self.crosshatch_strokes_coords[all_intersections[i][0]].append(
1392 all_intersections[i][3]
1393 ) # Save intersection coords
1394 else:
1395 self.is_crosshatch = False
1397 # Delete all duplicates
1398 bpy.ops.object.delete({"selected_objects": objects_to_delete})
1400 # If the main object has modifiers, turn their "viewport view status" to
1401 # what it was before the forced deactivation above
1402 if len(self.main_object.modifiers) > 0:
1403 for m_idx in range(len(self.main_object.modifiers)):
1404 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1406 self.update()
1408 return
1410 # Part of the Crosshatch process that is repeated when the operator is tweaked
1411 def crosshatch_surface_execute(self, context):
1412 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1413 # (without this the surface verts merging with the main object doesn't work well)
1414 self.modifiers_prev_viewport_state = []
1415 if len(self.main_object.modifiers) > 0:
1416 for m_idx in range(len(self.main_object.modifiers)):
1417 self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
1419 self.main_object.modifiers[m_idx].show_viewport = False
1421 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1423 me_name = "SURFSKIO_STK_TMP"
1424 me = bpy.data.meshes.new(me_name)
1426 all_verts_coords = []
1427 all_edges = []
1428 for st_idx in self.crosshatch_strokes_coords:
1429 for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
1430 coords = self.crosshatch_strokes_coords[st_idx][co_idx]
1432 all_verts_coords.append(coords)
1434 if co_idx > 0:
1435 all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
1437 me.from_pydata(all_verts_coords, all_edges, [])
1438 ob = object_utils.object_data_add(context, me)
1440 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1441 ob.select_set(True)
1442 bpy.context.view_layer.objects.active = ob
1444 # Get together each vert and its nearest, to the middle position
1445 verts = ob.data.vertices
1446 checked_verts = []
1447 for i in range(len(verts)):
1448 shortest_dist = None
1450 if i not in checked_verts:
1451 for t in range(len(verts)):
1452 if i != t and t not in checked_verts:
1453 dist = (verts[i].co - verts[t].co).length
1455 if shortest_dist is not None:
1456 if dist < shortest_dist:
1457 shortest_dist = dist
1458 nearest_vert = t
1459 else:
1460 shortest_dist = dist
1461 nearest_vert = t
1463 middle_location = (verts[i].co + verts[nearest_vert].co) / 2
1465 verts[i].co = middle_location
1466 verts[nearest_vert].co = middle_location
1468 checked_verts.append(i)
1469 checked_verts.append(nearest_vert)
1471 # Calculate average length between all the generated edges
1472 ob = bpy.context.object
1473 lengths_sum = 0
1474 for ed in ob.data.edges:
1475 v1 = ob.data.vertices[ed.vertices[0]]
1476 v2 = ob.data.vertices[ed.vertices[1]]
1478 lengths_sum += (v1.co - v2.co).length
1480 edges_count = len(ob.data.edges)
1481 # possible division by zero here
1482 average_edge_length = lengths_sum / edges_count if edges_count != 0 else 0.0001
1484 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1485 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1486 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1487 threshold=average_edge_length / 15.0)
1488 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1490 final_points_ob = bpy.context.view_layer.objects.active
1492 # Make a dictionary with the verts related to each vert
1493 related_key_verts = {}
1494 for ed in final_points_ob.data.edges:
1495 if not ed.vertices[0] in related_key_verts:
1496 related_key_verts[ed.vertices[0]] = []
1498 if not ed.vertices[1] in related_key_verts:
1499 related_key_verts[ed.vertices[1]] = []
1501 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
1502 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
1504 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
1505 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
1507 # Get groups of verts forming each face
1508 faces_verts_idx = []
1509 for v1 in related_key_verts: # verts-1 ....
1510 for v2 in related_key_verts: # verts-2
1511 if v1 != v2:
1512 related_verts_in_common = []
1513 v2_in_rel_v1 = False
1514 v1_in_rel_v2 = False
1515 for rel_v1 in related_key_verts[v1]:
1516 # Check if related verts of verts-1 are related verts of verts-2
1517 if rel_v1 in related_key_verts[v2]:
1518 related_verts_in_common.append(rel_v1)
1520 if v2 in related_key_verts[v1]:
1521 v2_in_rel_v1 = True
1523 if v1 in related_key_verts[v2]:
1524 v1_in_rel_v2 = True
1526 repeated_face = False
1527 # If two verts have two related verts in common, they form a quad
1528 if len(related_verts_in_common) == 2:
1529 # Check if the face is already saved
1530 for f_verts in faces_verts_idx:
1531 repeated_verts = 0
1533 if len(f_verts) == 4:
1534 if v1 in f_verts:
1535 repeated_verts += 1
1536 if v2 in f_verts:
1537 repeated_verts += 1
1538 if related_verts_in_common[0] in f_verts:
1539 repeated_verts += 1
1540 if related_verts_in_common[1] in f_verts:
1541 repeated_verts += 1
1543 if repeated_verts == len(f_verts):
1544 repeated_face = True
1545 break
1547 if not repeated_face:
1548 faces_verts_idx.append([v1, related_verts_in_common[0],
1549 v2, related_verts_in_common[1]])
1551 # If Two verts have one related vert in common and they are
1552 # related to each other, they form a triangle
1553 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1554 # Check if the face is already saved.
1555 for f_verts in faces_verts_idx:
1556 repeated_verts = 0
1558 if len(f_verts) == 3:
1559 if v1 in f_verts:
1560 repeated_verts += 1
1561 if v2 in f_verts:
1562 repeated_verts += 1
1563 if related_verts_in_common[0] in f_verts:
1564 repeated_verts += 1
1566 if repeated_verts == len(f_verts):
1567 repeated_face = True
1568 break
1570 if not repeated_face:
1571 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1573 # Keep only the faces that don't overlap by ignoring
1574 # quads that overlap with two adjacent triangles
1575 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1576 for i in range(len(faces_verts_idx)):
1577 for t in range(len(faces_verts_idx)):
1578 if i != t:
1579 verts_in_common = 0
1581 if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
1582 for v_idx in faces_verts_idx[t]:
1583 if v_idx in faces_verts_idx[i]:
1584 verts_in_common += 1
1585 # If it doesn't have all it's vertices repeated in the other face
1586 if verts_in_common == 3:
1587 if i not in faces_to_not_include_idx:
1588 faces_to_not_include_idx.append(i)
1590 # Build surface
1591 all_surface_verts_co = []
1592 for i in range(len(final_points_ob.data.vertices)):
1593 coords = final_points_ob.data.vertices[i].co
1594 all_surface_verts_co.append([coords[0], coords[1], coords[2]])
1596 # Verts of each face.
1597 all_surface_faces = []
1598 for i in range(len(faces_verts_idx)):
1599 if i not in faces_to_not_include_idx:
1600 face = []
1601 for v_idx in faces_verts_idx[i]:
1602 face.append(v_idx)
1604 all_surface_faces.append(face)
1606 # Build the mesh
1607 surf_me_name = "SURFSKIO_surface"
1608 me_surf = bpy.data.meshes.new(surf_me_name)
1609 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
1610 ob_surface = object_utils.object_data_add(context, me_surf)
1612 # Delete final points temporal object
1613 bpy.ops.object.delete({"selected_objects": [final_points_ob]})
1615 # Delete isolated verts if there are any
1616 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1617 ob_surface.select_set(True)
1618 bpy.context.view_layer.objects.active = ob_surface
1620 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1621 bpy.ops.mesh.select_all(action='DESELECT')
1622 bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
1623 bpy.ops.mesh.delete()
1624 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1626 # Join crosshatch results with original mesh
1628 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1629 edges_length_sum = 0
1630 for ed in ob_surface.data.edges:
1631 edges_length_sum += (
1632 ob_surface.data.vertices[ed.vertices[0]].co -
1633 ob_surface.data.vertices[ed.vertices[1]].co
1634 ).length
1636 # Make dictionary with all the verts connected to each vert, on the new surface object.
1637 surface_connected_verts = {}
1638 for ed in ob_surface.data.edges:
1639 if not ed.vertices[0] in surface_connected_verts:
1640 surface_connected_verts[ed.vertices[0]] = []
1642 surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
1644 if ed.vertices[1] not in surface_connected_verts:
1645 surface_connected_verts[ed.vertices[1]] = []
1647 surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
1649 # Duplicate the new surface object, and use shrinkwrap to
1650 # calculate later the nearest verts to the main object
1651 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1652 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1653 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1655 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1657 final_ob_duplicate = bpy.context.view_layer.objects.active
1659 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1660 shrinkwrap_modifier = final_ob_duplicate.modifiers[-1]
1661 shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
1662 shrinkwrap_modifier.target = self.main_object
1664 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier=shrinkwrap_modifier.name)
1666 # Make list with verts of original mesh as index and coords as value
1667 main_object_verts_coords = []
1668 for v in self.main_object.data.vertices:
1669 coords = self.main_object.matrix_world @ v.co
1671 # To avoid problems when taking "-0.00" as a different value as "0.00"
1672 for c in range(len(coords)):
1673 if "%.3f" % coords[c] == "-0.00":
1674 coords[c] = 0
1676 main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
1678 tuple(main_object_verts_coords)
1680 # Determine which verts will be merged, snap them to the nearest verts
1681 # on the original verts, and get them selected
1682 crosshatch_verts_to_merge = []
1683 if self.automatic_join:
1684 for i in range(len(ob_surface.data.vertices)-1):
1685 # Calculate the distance from each of the connected verts to the actual vert,
1686 # and compare it with the distance they would have if joined.
1687 # If they don't change much, that vert can be joined
1688 merge_actual_vert = True
1689 try:
1690 if len(surface_connected_verts[i]) < 4:
1691 for c_v_idx in surface_connected_verts[i]:
1692 points_original = []
1693 points_original.append(ob_surface.data.vertices[c_v_idx].co)
1694 points_original.append(ob_surface.data.vertices[i].co)
1696 points_target = []
1697 points_target.append(ob_surface.data.vertices[c_v_idx].co)
1698 points_target.append(final_ob_duplicate.data.vertices[i].co)
1700 vec_A = points_original[0] - points_original[1]
1701 vec_B = points_target[0] - points_target[1]
1703 dist_A = (points_original[0] - points_original[1]).length
1704 dist_B = (points_target[0] - points_target[1]).length
1706 if not (
1707 points_original[0] == points_original[1] or
1708 points_target[0] == points_target[1]
1709 ): # If any vector's length is zero
1711 angle = vec_A.angle(vec_B) / pi
1712 else:
1713 angle = 0
1715 # Set a range of acceptable variation in the connected edges
1716 if dist_B > dist_A * 1.7 * self.join_stretch_factor or \
1717 dist_B < dist_A / 2 / self.join_stretch_factor or \
1718 angle >= 0.15 * self.join_stretch_factor:
1720 merge_actual_vert = False
1721 break
1722 else:
1723 merge_actual_vert = False
1724 except:
1725 self.report({'WARNING'},
1726 "Crosshatch set incorrectly")
1728 if merge_actual_vert:
1729 coords = final_ob_duplicate.data.vertices[i].co
1730 # To avoid problems when taking "-0.000" as a different value as "0.00"
1731 for c in range(len(coords)):
1732 if "%.3f" % coords[c] == "-0.00":
1733 coords[c] = 0
1735 comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
1737 if comparison_coords in main_object_verts_coords:
1738 # Get the index of the vert with those coords in the main object
1739 main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords)
1741 if self.main_object.data.vertices[main_object_related_vert_idx].select is True or \
1742 self.main_object_selected_verts_count == 0:
1744 ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
1745 ob_surface.data.vertices[i].select = True
1746 crosshatch_verts_to_merge.append(i)
1748 # Make sure the vert in the main object is selected,
1749 # in case it wasn't selected and the "join crosshatch" option is active
1750 self.main_object.data.vertices[main_object_related_vert_idx].select = True
1752 # Delete duplicated object
1753 bpy.ops.object.delete({"selected_objects": [final_ob_duplicate]})
1755 # Join crosshatched surface and main object
1756 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1757 ob_surface.select_set(True)
1758 self.main_object.select_set(True)
1759 bpy.context.view_layer.objects.active = self.main_object
1761 bpy.ops.object.join('INVOKE_REGION_WIN')
1763 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1764 # Perform Remove doubles to merge verts
1765 if not (self.automatic_join is False and self.main_object_selected_verts_count == 0):
1766 bpy.ops.mesh.remove_doubles(threshold=0.0001)
1768 bpy.ops.mesh.select_all(action='DESELECT')
1770 # If the main object has modifiers, turn their "viewport view status"
1771 # to what it was before the forced deactivation above
1772 if len(self.main_object.modifiers) > 0:
1773 for m_idx in range(len(self.main_object.modifiers)):
1774 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1776 self.update()
1778 return {'FINISHED'}
1780 def rectangular_surface(self, context):
1781 # Selected edges
1782 all_selected_edges_idx = []
1783 all_selected_verts = []
1784 all_verts_idx = []
1785 for ed in self.main_object.data.edges:
1786 if ed.select:
1787 all_selected_edges_idx.append(ed.index)
1789 # Selected vertices
1790 if not ed.vertices[0] in all_selected_verts:
1791 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
1792 if not ed.vertices[1] in all_selected_verts:
1793 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
1795 # All verts (both from each edge) to determine later
1796 # which are at the tips (those not repeated twice)
1797 all_verts_idx.append(ed.vertices[0])
1798 all_verts_idx.append(ed.vertices[1])
1800 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1801 all_chains_tips_idx = []
1802 for v_idx in all_verts_idx:
1803 if all_verts_idx.count(v_idx) < 2:
1804 all_chains_tips_idx.append(v_idx)
1806 edges_connected_to_tips = []
1807 for ed in self.main_object.data.edges:
1808 if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and \
1809 not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
1811 edges_connected_to_tips.append(ed)
1813 # Check closed selections
1814 # List with groups of three verts, where the first element of the pair is
1815 # the unselected vert of a closed selection and the other two elements are the
1816 # selected neighbor verts (it will be useful to determine which selection chain
1817 # the unselected vert belongs to, and determine the "middle-vertex")
1818 single_unselected_verts_and_neighbors = []
1820 # To identify a "closed" selection (a selection that is a closed chain except
1821 # for one vertex) find the vertex in common that have the edges connected to tips.
1822 # If there is a vertex in common, that one is the unselected vert that closes
1823 # the selection or is a "middle-vertex"
1824 single_unselected_verts = []
1825 for ed in edges_connected_to_tips:
1826 for ed_b in edges_connected_to_tips:
1827 if ed != ed_b:
1828 if ed.vertices[0] == ed_b.vertices[0] and \
1829 not self.main_object.data.vertices[ed.vertices[0]].select and \
1830 ed.vertices[0] not in single_unselected_verts:
1832 # The second element is one of the tips of the selected
1833 # vertices of the closed selection
1834 single_unselected_verts_and_neighbors.append(
1835 [ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]
1837 single_unselected_verts.append(ed.vertices[0])
1838 break
1839 elif ed.vertices[0] == ed_b.vertices[1] and \
1840 not self.main_object.data.vertices[ed.vertices[0]].select and \
1841 ed.vertices[0] not in single_unselected_verts:
1843 single_unselected_verts_and_neighbors.append(
1844 [ed.vertices[0], ed.vertices[1], ed_b.vertices[0]]
1846 single_unselected_verts.append(ed.vertices[0])
1847 break
1848 elif ed.vertices[1] == ed_b.vertices[0] and \
1849 not self.main_object.data.vertices[ed.vertices[1]].select and \
1850 ed.vertices[1] not in single_unselected_verts:
1852 single_unselected_verts_and_neighbors.append(
1853 [ed.vertices[1], ed.vertices[0], ed_b.vertices[1]]
1855 single_unselected_verts.append(ed.vertices[1])
1856 break
1857 elif ed.vertices[1] == ed_b.vertices[1] and \
1858 not self.main_object.data.vertices[ed.vertices[1]].select and \
1859 ed.vertices[1] not in single_unselected_verts:
1861 single_unselected_verts_and_neighbors.append(
1862 [ed.vertices[1], ed.vertices[0], ed_b.vertices[0]]
1864 single_unselected_verts.append(ed.vertices[1])
1865 break
1867 middle_vertex_idx = None
1868 tips_to_discard_idx = []
1870 # Check if there is a "middle-vertex", and get its index
1871 for i in range(0, len(single_unselected_verts_and_neighbors)):
1872 actual_chain_verts = self.get_ordered_verts(
1873 self.main_object, all_selected_edges_idx,
1874 all_verts_idx, single_unselected_verts_and_neighbors[i][1],
1875 None, None
1878 if single_unselected_verts_and_neighbors[i][2] != \
1879 actual_chain_verts[len(actual_chain_verts) - 1].index:
1881 middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
1882 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
1883 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
1885 # List with pairs of verts that belong to the tips of each selection chain (row)
1886 verts_tips_same_chain_idx = []
1887 if len(all_chains_tips_idx) >= 2:
1888 checked_v = []
1889 for i in range(0, len(all_chains_tips_idx)):
1890 if all_chains_tips_idx[i] not in checked_v:
1891 v_chain = self.get_ordered_verts(
1892 self.main_object, all_selected_edges_idx,
1893 all_verts_idx, all_chains_tips_idx[i],
1894 middle_vertex_idx, None
1897 verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
1899 checked_v.append(v_chain[0].index)
1900 checked_v.append(v_chain[len(v_chain) - 1].index)
1902 # Selection tips (vertices).
1903 verts_tips_parsed_idx = []
1904 if len(all_chains_tips_idx) >= 2:
1905 for spec_v_idx in all_chains_tips_idx:
1906 if (spec_v_idx not in tips_to_discard_idx):
1907 verts_tips_parsed_idx.append(spec_v_idx)
1909 # Identify the type of selection made by the user
1910 if middle_vertex_idx is not None:
1911 # If there are 4 tips (two selection chains), and
1912 # there is only one single unselected vert (the middle vert)
1913 if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1:
1914 selection_type = "TWO_CONNECTED"
1915 else:
1916 # The type of the selection was not identified, the script stops.
1917 self.report({'WARNING'}, "The selection isn't valid.")
1919 self.stopping_errors = True
1921 return{'CANCELLED'}
1922 else:
1923 if len(all_chains_tips_idx) == 2: # If there are 2 tips
1924 selection_type = "SINGLE"
1925 elif len(all_chains_tips_idx) == 4: # If there are 4 tips
1926 selection_type = "TWO_NOT_CONNECTED"
1927 elif len(all_chains_tips_idx) == 0:
1928 if len(self.main_splines.data.splines) > 1:
1929 selection_type = "NO_SELECTION"
1930 else:
1931 # If the selection was not identified and there is only one stroke,
1932 # there's no possibility to build a surface, so the script is interrupted
1933 self.report({'WARNING'}, "The selection isn't valid.")
1935 self.stopping_errors = True
1937 return{'CANCELLED'}
1938 else:
1939 # The type of the selection was not identified, the script stops
1940 self.report({'WARNING'}, "The selection isn't valid.")
1942 self.stopping_errors = True
1944 return{'CANCELLED'}
1946 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1947 if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
1948 self.report({'WARNING'},
1949 "At least two strokes are needed when there are two not connected selections")
1951 self.stopping_errors = True
1953 return{'CANCELLED'}
1955 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1957 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1958 self.main_splines.select_set(True)
1959 bpy.context.view_layer.objects.active = self.main_splines
1961 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1962 bpy.ops.object.editmode_toggle('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.curve.smooth('INVOKE_REGION_WIN')
1969 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1971 self.selection_U_exists = False
1972 self.selection_U2_exists = False
1973 self.selection_V_exists = False
1974 self.selection_V2_exists = False
1976 self.selection_U_is_closed = False
1977 self.selection_U2_is_closed = False
1978 self.selection_V_is_closed = False
1979 self.selection_V2_is_closed = False
1981 # Define what vertices are at the tips of each selection and are not the middle-vertex
1982 if selection_type == "TWO_CONNECTED":
1983 self.selection_U_exists = True
1984 self.selection_V_exists = True
1986 closing_vert_U_idx = None
1987 closing_vert_V_idx = None
1988 closing_vert_U2_idx = None
1989 closing_vert_V2_idx = None
1991 # Determine which selection is Selection-U and which is Selection-V
1992 points_A = []
1993 points_B = []
1994 points_first_stroke_tips = []
1996 points_A.append(
1997 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[0]].co
1999 points_A.append(
2000 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
2002 points_B.append(
2003 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[1]].co
2005 points_B.append(
2006 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
2008 points_first_stroke_tips.append(
2009 self.main_splines.data.splines[0].bezier_points[0].co
2011 points_first_stroke_tips.append(
2012 self.main_splines.data.splines[0].bezier_points[
2013 len(self.main_splines.data.splines[0].bezier_points) - 1
2014 ].co
2017 angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
2018 angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
2020 if angle_A < angle_B:
2021 first_vert_U_idx = verts_tips_parsed_idx[0]
2022 first_vert_V_idx = verts_tips_parsed_idx[1]
2023 else:
2024 first_vert_U_idx = verts_tips_parsed_idx[1]
2025 first_vert_V_idx = verts_tips_parsed_idx[0]
2027 elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
2028 first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
2029 last_sketched_point_first_stroke_co = \
2030 self.main_splines.data.splines[0].bezier_points[
2031 len(self.main_splines.data.splines[0].bezier_points) - 1
2032 ].co
2033 first_sketched_point_last_stroke_co = \
2034 self.main_splines.data.splines[
2035 len(self.main_splines.data.splines) - 1
2036 ].bezier_points[0].co
2037 if len(self.main_splines.data.splines) > 1:
2038 first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
2039 last_sketched_point_second_stroke_co = \
2040 self.main_splines.data.splines[1].bezier_points[
2041 len(self.main_splines.data.splines[1].bezier_points) - 1
2042 ].co
2044 single_unselected_neighbors = [] # Only the neighbors of the single unselected verts
2045 for verts_neig_idx in single_unselected_verts_and_neighbors:
2046 single_unselected_neighbors.append(verts_neig_idx[1])
2047 single_unselected_neighbors.append(verts_neig_idx[2])
2049 all_chains_tips_and_middle_vert = []
2050 for v_idx in all_chains_tips_idx:
2051 if v_idx not in single_unselected_neighbors:
2052 all_chains_tips_and_middle_vert.append(v_idx)
2054 all_chains_tips_and_middle_vert += single_unselected_verts
2056 all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
2058 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2059 nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = \
2060 self.shortest_distance(
2061 self.main_object,
2062 first_sketched_point_first_stroke_co,
2063 all_chains_tips_and_middle_vert
2065 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2066 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2067 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2069 nearest_tip_to_first_st_first_pt_opposite_idx = \
2070 self.opposite_tip(
2071 nearest_tip_to_first_st_first_pt_idx,
2072 verts_tips_same_chain_idx
2074 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2075 nearest_tip_to_first_st_last_pt_idx, _temp_dist = \
2076 self.shortest_distance(
2077 self.main_object,
2078 last_sketched_point_first_stroke_co,
2079 all_chains_tips_and_middle_vert
2081 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2082 nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = \
2083 self.shortest_distance(
2084 self.main_object,
2085 first_sketched_point_last_stroke_co,
2086 all_chains_tips_and_middle_vert
2088 if len(self.main_splines.data.splines) > 1:
2089 # The selected vertex nearest to the first point of the second sketched stroke
2090 # (This will be useful to determine the direction of the closed
2091 # selection V when extruding along strokes)
2092 nearest_vert_to_second_st_first_pt_idx, _temp_dist = \
2093 self.shortest_distance(
2094 self.main_object,
2095 first_sketched_point_second_stroke_co,
2096 all_verts_idx
2098 # The selected vertex nearest to the first point of the second sketched stroke
2099 # (This will be useful to determine the direction of the closed
2100 # selection V2 when extruding along strokes)
2101 nearest_vert_to_second_st_last_pt_idx, _temp_dist = \
2102 self.shortest_distance(
2103 self.main_object,
2104 last_sketched_point_second_stroke_co,
2105 all_verts_idx
2107 # Determine if the single selection will be treated as U or as V
2108 edges_sum = 0
2109 for i in all_selected_edges_idx:
2110 edges_sum += (
2111 (self.main_object.matrix_world @
2112 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) -
2113 (self.main_object.matrix_world @
2114 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)
2115 ).length
2117 average_edge_length = edges_sum / len(all_selected_edges_idx)
2119 # Get shortest distance from the first point of the last stroke to any participating vertex
2120 _temp_idx, shortest_distance_to_last_stroke = \
2121 self.shortest_distance(
2122 self.main_object,
2123 first_sketched_point_last_stroke_co,
2124 all_participating_verts
2126 # If the beginning of the first stroke is near enough, and its orientation
2127 # difference with the first edge of the nearest selection chain is not too high,
2128 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2129 if shortest_distance_to_first_stroke < average_edge_length / 4 and \
2130 shortest_distance_to_last_stroke < average_edge_length and \
2131 len(self.main_splines.data.splines) > 1:
2133 self.selection_U_exists = False
2134 self.selection_V_exists = True
2135 # If the first selection is not closed
2136 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2137 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2138 self.selection_V_is_closed = False
2139 closing_vert_U_idx = None
2140 closing_vert_U2_idx = None
2141 closing_vert_V_idx = None
2142 closing_vert_V2_idx = None
2144 first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2146 if selection_type == "TWO_NOT_CONNECTED":
2147 self.selection_V2_exists = True
2149 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2150 else:
2151 self.selection_V_is_closed = True
2152 closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2154 # Get the neighbors of the first (unselected) vert of the closed selection U.
2155 vert_neighbors = []
2156 for verts in single_unselected_verts_and_neighbors:
2157 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2158 vert_neighbors.append(verts[1])
2159 vert_neighbors.append(verts[2])
2160 break
2162 verts_V = self.get_ordered_verts(
2163 self.main_object, all_selected_edges_idx,
2164 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2167 for i in range(0, len(verts_V)):
2168 if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
2169 # If the vertex nearest to the first point of the second stroke
2170 # is in the first half of the selected verts
2171 if i >= len(verts_V) / 2:
2172 first_vert_V_idx = vert_neighbors[1]
2173 break
2174 else:
2175 first_vert_V_idx = vert_neighbors[0]
2176 break
2178 if selection_type == "TWO_NOT_CONNECTED":
2179 self.selection_V2_exists = True
2180 # If the second selection is not closed
2181 if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or \
2182 nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx:
2184 self.selection_V2_is_closed = False
2185 closing_vert_V2_idx = None
2186 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2188 else:
2189 self.selection_V2_is_closed = True
2190 closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2192 # Get the neighbors of the first (unselected) vert of the closed selection U
2193 vert_neighbors = []
2194 for verts in single_unselected_verts_and_neighbors:
2195 if verts[0] == nearest_tip_to_first_st_last_pt_idx:
2196 vert_neighbors.append(verts[1])
2197 vert_neighbors.append(verts[2])
2198 break
2200 verts_V2 = self.get_ordered_verts(
2201 self.main_object, all_selected_edges_idx,
2202 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2205 for i in range(0, len(verts_V2)):
2206 if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
2207 # If the vertex nearest to the first point of the second stroke
2208 # is in the first half of the selected verts
2209 if i >= len(verts_V2) / 2:
2210 first_vert_V2_idx = vert_neighbors[1]
2211 break
2212 else:
2213 first_vert_V2_idx = vert_neighbors[0]
2214 break
2215 else:
2216 self.selection_V2_exists = False
2218 else:
2219 self.selection_U_exists = True
2220 self.selection_V_exists = False
2221 # If the first selection is not closed
2222 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2223 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2224 self.selection_U_is_closed = False
2225 closing_vert_U_idx = None
2227 points_tips = []
2228 points_tips.append(
2229 self.main_object.matrix_world @
2230 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2232 points_tips.append(
2233 self.main_object.matrix_world @
2234 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co
2236 points_first_stroke_tips = []
2237 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2238 points_first_stroke_tips.append(
2239 self.main_splines.data.splines[0].bezier_points[
2240 len(self.main_splines.data.splines[0].bezier_points) - 1
2241 ].co
2243 vec_A = points_tips[0] - points_tips[1]
2244 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2246 # Compare the direction of the selection and the first
2247 # grease pencil stroke to determine which is the "first" vertex of the selection
2248 if vec_A.dot(vec_B) < 0:
2249 first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
2250 else:
2251 first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2253 else:
2254 self.selection_U_is_closed = True
2255 closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2257 # Get the neighbors of the first (unselected) vert of the closed selection U
2258 vert_neighbors = []
2259 for verts in single_unselected_verts_and_neighbors:
2260 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2261 vert_neighbors.append(verts[1])
2262 vert_neighbors.append(verts[2])
2263 break
2265 points_first_and_neighbor = []
2266 points_first_and_neighbor.append(
2267 self.main_object.matrix_world @
2268 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2270 points_first_and_neighbor.append(
2271 self.main_object.matrix_world @
2272 self.main_object.data.vertices[vert_neighbors[0]].co
2274 points_first_stroke_tips = []
2275 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2276 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
2278 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2279 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2281 # Compare the direction of the selection and the first grease pencil stroke to
2282 # determine which is the vertex neighbor to the first vertex (unselected) of
2283 # the closed selection. This will determine the direction of the closed selection
2284 if vec_A.dot(vec_B) < 0:
2285 first_vert_U_idx = vert_neighbors[1]
2286 else:
2287 first_vert_U_idx = vert_neighbors[0]
2289 if selection_type == "TWO_NOT_CONNECTED":
2290 self.selection_U2_exists = True
2291 # If the second selection is not closed
2292 if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or \
2293 nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx:
2295 self.selection_U2_is_closed = False
2296 closing_vert_U2_idx = None
2297 first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2298 else:
2299 self.selection_U2_is_closed = True
2300 closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2302 # Get the neighbors of the first (unselected) vert of the closed selection U
2303 vert_neighbors = []
2304 for verts in single_unselected_verts_and_neighbors:
2305 if verts[0] == nearest_tip_to_last_st_first_pt_idx:
2306 vert_neighbors.append(verts[1])
2307 vert_neighbors.append(verts[2])
2308 break
2310 points_first_and_neighbor = []
2311 points_first_and_neighbor.append(
2312 self.main_object.matrix_world @
2313 self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co
2315 points_first_and_neighbor.append(
2316 self.main_object.matrix_world @
2317 self.main_object.data.vertices[vert_neighbors[0]].co
2319 points_last_stroke_tips = []
2320 points_last_stroke_tips.append(
2321 self.main_splines.data.splines[
2322 len(self.main_splines.data.splines) - 1
2323 ].bezier_points[0].co
2325 points_last_stroke_tips.append(
2326 self.main_splines.data.splines[
2327 len(self.main_splines.data.splines) - 1
2328 ].bezier_points[1].co
2330 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2331 vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
2333 # Compare the direction of the selection and the last grease pencil stroke to
2334 # determine which is the vertex neighbor to the first vertex (unselected) of
2335 # the closed selection. This will determine the direction of the closed selection
2336 if vec_A.dot(vec_B) < 0:
2337 first_vert_U2_idx = vert_neighbors[1]
2338 else:
2339 first_vert_U2_idx = vert_neighbors[0]
2340 else:
2341 self.selection_U2_exists = False
2343 elif selection_type == "NO_SELECTION":
2344 self.selection_U_exists = False
2345 self.selection_V_exists = False
2347 # Get an ordered list of the vertices of Selection-U
2348 verts_ordered_U = []
2349 if self.selection_U_exists:
2350 verts_ordered_U = self.get_ordered_verts(
2351 self.main_object, all_selected_edges_idx,
2352 all_verts_idx, first_vert_U_idx,
2353 middle_vertex_idx, closing_vert_U_idx
2356 # Get an ordered list of the vertices of Selection-U2
2357 verts_ordered_U2 = []
2358 if self.selection_U2_exists:
2359 verts_ordered_U2 = self.get_ordered_verts(
2360 self.main_object, all_selected_edges_idx,
2361 all_verts_idx, first_vert_U2_idx,
2362 middle_vertex_idx, closing_vert_U2_idx
2365 # Get an ordered list of the vertices of Selection-V
2366 verts_ordered_V = []
2367 if self.selection_V_exists:
2368 verts_ordered_V = self.get_ordered_verts(
2369 self.main_object, all_selected_edges_idx,
2370 all_verts_idx, first_vert_V_idx,
2371 middle_vertex_idx, closing_vert_V_idx
2373 verts_ordered_V_indices = [x.index for x in verts_ordered_V]
2375 # Get an ordered list of the vertices of Selection-V2
2376 verts_ordered_V2 = []
2377 if self.selection_V2_exists:
2378 verts_ordered_V2 = self.get_ordered_verts(
2379 self.main_object, all_selected_edges_idx,
2380 all_verts_idx, first_vert_V2_idx,
2381 middle_vertex_idx, closing_vert_V2_idx
2384 # Check if when there are two-not-connected selections both have the same
2385 # number of verts. If not terminate the script
2386 if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or
2387 (self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
2388 # Display a warning
2389 self.report({'WARNING'}, "Both selections must have the same number of edges")
2391 self.stopping_errors = True
2393 return{'CANCELLED'}
2395 # Calculate edges U proportions
2396 # Sum selected edges U lengths
2397 edges_lengths_U = []
2398 edges_lengths_sum_U = 0
2400 if self.selection_U_exists:
2401 edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(
2402 self.main_object,
2403 verts_ordered_U
2405 if self.selection_U2_exists:
2406 edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(
2407 self.main_object,
2408 verts_ordered_U2
2410 # Sum selected edges V lengths
2411 edges_lengths_V = []
2412 edges_lengths_sum_V = 0
2414 if self.selection_V_exists:
2415 edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(
2416 self.main_object,
2417 verts_ordered_V
2419 if self.selection_V2_exists:
2420 edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(
2421 self.main_object,
2422 verts_ordered_V2
2425 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2426 bpy.ops.curve.subdivide('INVOKE_REGION_WIN',
2427 number_cuts=bpy.context.scene.bsurfaces.SURFSK_precision)
2428 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2430 # Proportions U
2431 edges_proportions_U = []
2432 edges_proportions_U = self.get_edges_proportions(
2433 edges_lengths_U, edges_lengths_sum_U,
2434 self.selection_U_exists, self.edges_U
2436 verts_count_U = len(edges_proportions_U) + 1
2438 if self.selection_U2_exists:
2439 edges_proportions_U2 = []
2440 edges_proportions_U2 = self.get_edges_proportions(
2441 edges_lengths_U2, edges_lengths_sum_U2,
2442 self.selection_U2_exists, self.edges_V
2445 # Proportions V
2446 edges_proportions_V = []
2447 edges_proportions_V = self.get_edges_proportions(
2448 edges_lengths_V, edges_lengths_sum_V,
2449 self.selection_V_exists, self.edges_V
2452 if self.selection_V2_exists:
2453 edges_proportions_V2 = []
2454 edges_proportions_V2 = self.get_edges_proportions(
2455 edges_lengths_V2, edges_lengths_sum_V2,
2456 self.selection_V2_exists, self.edges_V
2459 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2460 # the actual sketched curves with a "closing segment"
2461 if self.cyclic_follow and not self.selection_V_exists and not \
2462 ((self.selection_U_exists and not self.selection_U_is_closed) or
2463 (self.selection_U2_exists and not self.selection_U2_is_closed)):
2465 simplified_spline_coords = []
2466 simplified_curve = []
2467 ob_simplified_curve = []
2468 splines_first_v_co = []
2469 for i in range(len(self.main_splines.data.splines)):
2470 # Create a curve object for the actual spline "cyclic extension"
2471 simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
2472 ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
2473 bpy.context.collection.objects.link(ob_simplified_curve[i])
2475 simplified_curve[i].dimensions = "3D"
2477 spline_coords = []
2478 for bp in self.main_splines.data.splines[i].bezier_points:
2479 spline_coords.append(bp.co)
2481 # Simplification
2482 simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
2484 # Get the coordinates of the first vert of the actual spline
2485 splines_first_v_co.append(simplified_spline_coords[i][0])
2487 # Generate the spline
2488 spline = simplified_curve[i].splines.new('BEZIER')
2489 # less one because one point is added when the spline is created
2490 spline.bezier_points.add(len(simplified_spline_coords[i]) - 1)
2491 for p in range(0, len(simplified_spline_coords[i])):
2492 spline.bezier_points[p].co = simplified_spline_coords[i][p]
2494 spline.use_cyclic_u = True
2496 spline_bp_count = len(spline.bezier_points)
2498 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2499 ob_simplified_curve[i].select_set(True)
2500 bpy.context.view_layer.objects.active = ob_simplified_curve[i]
2502 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2503 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
2504 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2505 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
2506 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2508 # Select the "closing segment", and subdivide it
2509 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
2510 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
2511 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
2513 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
2514 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
2515 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
2517 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2518 segments = sqrt(
2519 (ob_simplified_curve[i].data.splines[0].bezier_points[0].co -
2520 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length /
2521 self.average_gp_segment_length
2523 for t in range(2):
2524 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=segments)
2526 # Delete the other vertices and make it non-cyclic to
2527 # keep only the needed verts of the "closing segment"
2528 bpy.ops.curve.select_all(action='INVERT')
2529 bpy.ops.curve.delete(type='VERT')
2530 ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
2531 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2533 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2534 first_new_index = len(self.main_splines.data.splines[i].bezier_points)
2535 self.main_splines.data.splines[i].bezier_points.add(
2536 len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1
2538 for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
2539 self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = \
2540 ob_simplified_curve[i].data.splines[0].bezier_points[t].co
2542 # Delete the temporal curve
2543 bpy.ops.object.delete({"selected_objects": [ob_simplified_curve[i]]})
2545 # Get the coords of the points distributed along the sketched strokes,
2546 # with proportions-U of the first selection
2547 pts_on_strokes_with_proportions_U = self.distribute_pts(
2548 self.main_splines.data.splines,
2549 edges_proportions_U
2551 sketched_splines_parsed = []
2553 if self.selection_U2_exists:
2554 # Initialize the multidimensional list with the proportions of all the segments
2555 proportions_loops_crossing_strokes = []
2556 for i in range(len(pts_on_strokes_with_proportions_U)):
2557 proportions_loops_crossing_strokes.append([])
2559 for t in range(len(pts_on_strokes_with_proportions_U[0])):
2560 proportions_loops_crossing_strokes[i].append(None)
2562 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2563 for lp in range(len(pts_on_strokes_with_proportions_U[0])):
2564 loop_segments_lengths = []
2566 for st in range(len(pts_on_strokes_with_proportions_U)):
2567 # When on the first stroke, add the segment from the selection to the dirst stroke
2568 if st == 0:
2569 loop_segments_lengths.append(
2570 ((self.main_object.matrix_world @ verts_ordered_U[lp].co) -
2571 pts_on_strokes_with_proportions_U[0][lp]).length
2573 # For all strokes except for the last, calculate the distance
2574 # from the actual stroke to the next
2575 if st != len(pts_on_strokes_with_proportions_U) - 1:
2576 loop_segments_lengths.append(
2577 (pts_on_strokes_with_proportions_U[st][lp] -
2578 pts_on_strokes_with_proportions_U[st + 1][lp]).length
2580 # When on the last stroke, add the segments
2581 # from the last stroke to the second selection
2582 if st == len(pts_on_strokes_with_proportions_U) - 1:
2583 loop_segments_lengths.append(
2584 (pts_on_strokes_with_proportions_U[st][lp] -
2585 (self.main_object.matrix_world @ verts_ordered_U2[lp].co)).length
2587 # Calculate full loop length
2588 loop_seg_lengths_sum = 0
2589 for i in range(len(loop_segments_lengths)):
2590 loop_seg_lengths_sum += loop_segments_lengths[i]
2592 # Fill the multidimensional list with the proportions of all the segments
2593 for st in range(len(pts_on_strokes_with_proportions_U)):
2594 proportions_loops_crossing_strokes[st][lp] = \
2595 loop_segments_lengths[st] / loop_seg_lengths_sum
2597 # Calculate proportions for each stroke
2598 for st in range(len(pts_on_strokes_with_proportions_U)):
2599 actual_stroke_spline = []
2600 # Needs to be a list for the "distribute_pts" method
2601 actual_stroke_spline.append(self.main_splines.data.splines[st])
2603 # Calculate the proportions for the actual stroke.
2604 actual_edges_proportions_U = []
2605 for i in range(len(edges_proportions_U)):
2606 proportions_sum = 0
2608 # Sum the proportions of this loop up to the actual.
2609 for t in range(0, st + 1):
2610 proportions_sum += proportions_loops_crossing_strokes[t][i]
2611 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2612 # and the proportions refer to edges, so we start at the element 1
2613 # of proportions_loops_crossing_strokes instead of element 0
2614 actual_edges_proportions_U.append(
2615 edges_proportions_U[i] -
2616 ((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)
2618 points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
2619 sketched_splines_parsed.append(points_actual_spline[0])
2620 else:
2621 sketched_splines_parsed = pts_on_strokes_with_proportions_U
2623 # If the selection type is "TWO_NOT_CONNECTED" replace the
2624 # points of the last spline with the points in the "target" selection
2625 if selection_type == "TWO_NOT_CONNECTED":
2626 if self.selection_U2_exists:
2627 for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
2628 sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = \
2629 self.main_object.matrix_world @ verts_ordered_U2[i].co
2631 # Create temporary curves along the "control-points" found
2632 # on the sketched curves and the mesh selection
2633 mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
2634 me = bpy.data.meshes.new(mesh_ctrl_pts_name)
2635 ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
2636 ob_ctrl_pts.data = me
2637 bpy.context.collection.objects.link(ob_ctrl_pts)
2639 cyclic_loops_U = []
2640 first_verts = []
2641 second_verts = []
2642 last_verts = []
2644 for i in range(0, verts_count_U):
2645 vert_num_in_spline = 1
2647 if self.selection_U_exists:
2648 ob_ctrl_pts.data.vertices.add(1)
2649 last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2650 last_v.co = self.main_object.matrix_world @ verts_ordered_U[i].co
2652 vert_num_in_spline += 1
2654 for t in range(0, len(sketched_splines_parsed)):
2655 ob_ctrl_pts.data.vertices.add(1)
2656 v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2657 v.co = sketched_splines_parsed[t][i]
2659 if vert_num_in_spline > 1:
2660 ob_ctrl_pts.data.edges.add(1)
2661 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = \
2662 len(ob_ctrl_pts.data.vertices) - 2
2663 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = \
2664 len(ob_ctrl_pts.data.vertices) - 1
2666 if t == 0:
2667 first_verts.append(v.index)
2669 if t == 1:
2670 second_verts.append(v.index)
2672 if t == len(sketched_splines_parsed) - 1:
2673 last_verts.append(v.index)
2675 last_v = v
2676 vert_num_in_spline += 1
2678 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2679 ob_ctrl_pts.select_set(True)
2680 bpy.context.view_layer.objects.active = ob_ctrl_pts
2682 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2683 bpy.ops.mesh.select_all(action='DESELECT')
2684 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2686 # Determine which loops-U will be "Cyclic"
2687 for i in range(0, len(first_verts)):
2688 # When there is Cyclic Cross there is no need of
2689 # Automatic Join, (and there are at least three strokes)
2690 if self.automatic_join and not self.cyclic_cross and \
2691 selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3:
2693 v = ob_ctrl_pts.data.vertices
2694 first_point_co = v[first_verts[i]].co
2695 second_point_co = v[second_verts[i]].co
2696 last_point_co = v[last_verts[i]].co
2698 # Coordinates of the point in the center of both the first and last verts.
2699 verts_center_co = [
2700 (first_point_co[0] + last_point_co[0]) / 2,
2701 (first_point_co[1] + last_point_co[1]) / 2,
2702 (first_point_co[2] + last_point_co[2]) / 2
2704 vec_A = second_point_co - first_point_co
2705 vec_B = second_point_co - Vector(verts_center_co)
2707 # Calculate the length of the first segment of the loop,
2708 # and the length it would have after moving the first vert
2709 # to the middle position between first and last
2710 length_original = (second_point_co - first_point_co).length
2711 length_target = (second_point_co - Vector(verts_center_co)).length
2713 angle = vec_A.angle(vec_B) / pi
2715 # If the target length doesn't stretch too much, and the
2716 # its angle doesn't change to much either
2717 if length_target <= length_original * 1.03 * self.join_stretch_factor and \
2718 angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists:
2720 cyclic_loops_U.append(True)
2721 # Move the first vert to the center coordinates
2722 ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
2723 # Select the last verts from Cyclic loops, for later deletion all at once
2724 v[last_verts[i]].select = True
2725 else:
2726 cyclic_loops_U.append(False)
2727 else:
2728 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2729 if self.cyclic_cross and not self.selection_U_exists and not \
2730 ((self.selection_V_exists and not self.selection_V_is_closed) or
2731 (self.selection_V2_exists and not self.selection_V2_is_closed)):
2733 cyclic_loops_U.append(True)
2734 else:
2735 cyclic_loops_U.append(False)
2737 # The cyclic_loops_U list needs to be reversed.
2738 cyclic_loops_U.reverse()
2740 # Delete the previously selected (last_)verts.
2741 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2742 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
2743 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2745 # Create curves from control points.
2746 bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
2747 ob_curves_surf = bpy.context.view_layer.objects.active
2748 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2749 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2750 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2752 # Make Cyclic the splines designated as Cyclic.
2753 for i in range(0, len(cyclic_loops_U)):
2754 ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
2756 # Get the coords of all points on first loop-U, for later comparison with its
2757 # subdivided version, to know which points of the loops-U are crossed by the
2758 # original strokes. The indices will be the same for the other loops-U
2759 if self.loops_on_strokes:
2760 coords_loops_U_control_points = []
2761 for p in ob_ctrl_pts.data.splines[0].bezier_points:
2762 coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
2764 tuple(coords_loops_U_control_points)
2766 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2767 if self.loops_on_strokes and not self.selection_V_exists:
2768 edges_V_count = len(self.main_splines.data.splines) * self.edges_V
2769 else:
2770 edges_V_count = len(edges_proportions_V)
2772 # The Follow precision will vary depending on the number of Follow face-loops
2773 precision_multiplier = round(2 + (edges_V_count / 15))
2774 curve_cuts = bpy.context.scene.bsurfaces.SURFSK_precision * precision_multiplier
2776 # Subdivide the curves
2777 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=curve_cuts)
2779 # The verts position shifting that happens with splines subdivision.
2780 # For later reorder splines points
2781 verts_position_shift = curve_cuts + 1
2782 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2784 # Reorder coordinates of the points of each spline to put the first point of
2785 # the spline starting at the position it was the first point before sudividing
2786 # the curve. And make a new curve object per spline (to handle memory better later)
2787 splines_U_objects = []
2788 for i in range(len(ob_curves_surf.data.splines)):
2789 spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
2790 ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
2791 bpy.context.collection.objects.link(ob_spline_U)
2793 spline_U_curve.dimensions = "3D"
2795 # Add points to the spline in the new curve object
2796 ob_spline_U.data.splines.new('BEZIER')
2797 for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
2798 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2799 if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
2800 point_index = t + verts_position_shift
2801 else:
2802 point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
2803 else:
2804 point_index = t
2805 # to avoid adding the first point since it's added when the spline is created
2806 if t > 0:
2807 ob_spline_U.data.splines[0].bezier_points.add(1)
2808 ob_spline_U.data.splines[0].bezier_points[t].co = \
2809 ob_curves_surf.data.splines[i].bezier_points[point_index].co
2811 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2812 # Add a last point at the same location as the first one
2813 ob_spline_U.data.splines[0].bezier_points.add(1)
2814 ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = \
2815 ob_spline_U.data.splines[0].bezier_points[0].co
2816 else:
2817 ob_spline_U.data.splines[0].use_cyclic_u = False
2819 splines_U_objects.append(ob_spline_U)
2820 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2821 ob_spline_U.select_set(True)
2822 bpy.context.view_layer.objects.active = ob_spline_U
2824 # When option "Loops on strokes" is active each "Cross" loop will have
2825 # its own proportions according to where the original strokes "touch" them
2826 if self.loops_on_strokes:
2827 # Get the indices of points where the original strokes "touch" loops-U
2828 points_U_crossed_by_strokes = []
2829 for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
2830 bp = splines_U_objects[0].data.splines[0].bezier_points[i]
2831 if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
2832 points_U_crossed_by_strokes.append(i)
2834 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2835 edge_order_number_for_splines = {}
2836 if self.selection_V_exists:
2837 # For two-connected selections add a first hypothetic stroke at the beginning.
2838 if selection_type == "TWO_CONNECTED":
2839 edge_order_number_for_splines[0] = 0
2841 for i in range(len(self.main_splines.data.splines)):
2842 sp = self.main_splines.data.splines[i]
2843 v_idx, _dist_temp = self.shortest_distance(
2844 self.main_object,
2845 sp.bezier_points[0].co,
2846 verts_ordered_V_indices
2848 # Get the position (edges count) of the vert v_idx in the selected chain V
2849 edge_idx_in_chain = verts_ordered_V_indices.index(v_idx)
2851 # For two-connected selections the strokes go after the
2852 # hypothetic stroke added before, so the index adds one per spline
2853 if selection_type == "TWO_CONNECTED":
2854 spline_number = i + 1
2855 else:
2856 spline_number = i
2858 edge_order_number_for_splines[spline_number] = edge_idx_in_chain
2860 # Get the first and last verts indices for later comparison
2861 if i == 0:
2862 first_v_idx = v_idx
2863 elif i == len(self.main_splines.data.splines) - 1:
2864 last_v_idx = v_idx
2866 if self.selection_V_is_closed:
2867 # If there is no last stroke on the last vertex (same as first vertex),
2868 # add a hypothetic spline at last vert order
2869 if first_v_idx != last_v_idx:
2870 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2871 len(verts_ordered_V_indices) - 1
2872 else:
2873 if self.cyclic_cross:
2874 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2875 len(verts_ordered_V_indices) - 2
2876 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2877 len(verts_ordered_V_indices) - 1
2878 else:
2879 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2880 len(verts_ordered_V_indices) - 1
2882 # Get the coords of the points distributed along the
2883 # "crossing curves", with appropriate proportions-V
2884 surface_splines_parsed = []
2885 for i in range(len(splines_U_objects)):
2886 sp_ob = splines_U_objects[i]
2887 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2888 if self.loops_on_strokes:
2889 # Segments distances from stroke to stroke
2890 dist = 0
2891 full_dist = 0
2892 segments_distances = []
2893 for t in range(len(sp_ob.data.splines[0].bezier_points)):
2894 bp = sp_ob.data.splines[0].bezier_points[t]
2896 if t == 0:
2897 last_p = bp.co
2898 else:
2899 actual_p = bp.co
2900 dist += (last_p - actual_p).length
2902 if t in points_U_crossed_by_strokes:
2903 segments_distances.append(dist)
2904 full_dist += dist
2906 dist = 0
2908 last_p = actual_p
2910 # Calculate Proportions.
2911 used_edges_proportions_V = []
2912 for t in range(len(segments_distances)):
2913 if self.selection_V_exists:
2914 if t == 0:
2915 order_number_last_stroke = 0
2917 segment_edges_length_V = 0
2918 segment_edges_length_V2 = 0
2919 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2920 segment_edges_length_V += edges_lengths_V[order]
2921 if self.selection_V2_exists:
2922 segment_edges_length_V2 += edges_lengths_V2[order]
2924 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2925 # Calculate each "sub-segment" (the ones between each stroke) length
2926 if self.selection_V2_exists:
2927 proportion_sub_seg = (edges_lengths_V2[order] -
2928 ((edges_lengths_V2[order] - edges_lengths_V[order]) /
2929 len(splines_U_objects) * i)) / (segment_edges_length_V2 -
2930 (segment_edges_length_V2 - segment_edges_length_V) /
2931 len(splines_U_objects) * i)
2933 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2934 else:
2935 proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
2936 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2938 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2940 order_number_last_stroke = edge_order_number_for_splines[t + 1]
2942 else:
2943 for _c in range(self.edges_V):
2944 # Calculate each "sub-segment" (the ones between each stroke) length
2945 sub_seg_dist = segments_distances[t] / self.edges_V
2946 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2948 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2949 surface_splines_parsed.append(actual_spline[0])
2951 else:
2952 if self.selection_V2_exists:
2953 used_edges_proportions_V = []
2954 for p in range(len(edges_proportions_V)):
2955 used_edges_proportions_V.append(
2956 edges_proportions_V2[p] -
2957 ((edges_proportions_V2[p] -
2958 edges_proportions_V[p]) / len(splines_U_objects) * i)
2960 else:
2961 used_edges_proportions_V = edges_proportions_V
2963 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2964 surface_splines_parsed.append(actual_spline[0])
2966 # Set the verts of the first and last splines to the locations
2967 # of the respective verts in the selections
2968 if self.selection_V_exists:
2969 for i in range(0, len(surface_splines_parsed[0])):
2970 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = \
2971 self.main_object.matrix_world @ verts_ordered_V[i].co
2973 if selection_type == "TWO_NOT_CONNECTED":
2974 if self.selection_V2_exists:
2975 for i in range(0, len(surface_splines_parsed[0])):
2976 surface_splines_parsed[0][i] = self.main_object.matrix_world @ verts_ordered_V2[i].co
2978 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2979 # merge the verts of the tips of the loops when they are "near enough"
2980 if self.automatic_join and selection_type != "TWO_CONNECTED":
2981 # Join the tips of "Follow" loops that are near enough and must be "closed"
2982 if not self.selection_V_exists and len(edges_proportions_U) >= 3:
2983 for i in range(len(surface_splines_parsed[0])):
2984 sp = surface_splines_parsed
2985 loop_segment_dist = (sp[0][i] - sp[1][i]).length
2987 verts_middle_position_co = [
2988 (sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2,
2989 (sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2,
2990 (sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2
2992 points_original = []
2993 points_original.append(sp[1][i])
2994 points_original.append(sp[0][i])
2996 points_target = []
2997 points_target.append(sp[1][i])
2998 points_target.append(Vector(verts_middle_position_co))
3000 vec_A = points_original[0] - points_original[1]
3001 vec_B = points_target[0] - points_target[1]
3002 # check for zero angles, not sure if it is a great fix
3003 if vec_A.length != 0 and vec_B.length != 0:
3004 angle = vec_A.angle(vec_B) / pi
3005 edge_new_length = (Vector(verts_middle_position_co) - sp[1][i]).length
3006 else:
3007 angle = 0
3008 edge_new_length = 0
3010 # If after moving the verts to the middle point, the segment doesn't stretch too much
3011 if edge_new_length <= loop_segment_dist * 1.5 * \
3012 self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor:
3014 # Avoid joining when the actual loop must be merged with the original mesh
3015 if not (self.selection_U_exists and i == 0) and \
3016 not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1):
3018 # Change the coords of both verts to the middle position
3019 surface_splines_parsed[0][i] = verts_middle_position_co
3020 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
3022 # Delete object with control points and object from grease pencil conversion
3023 bpy.ops.object.delete({"selected_objects": [ob_ctrl_pts]})
3025 bpy.ops.object.delete({"selected_objects": splines_U_objects})
3027 # Generate surface
3029 # Get all verts coords
3030 all_surface_verts_co = []
3031 for i in range(0, len(surface_splines_parsed)):
3032 # Get coords of all verts and make a list with them
3033 for pt_co in surface_splines_parsed[i]:
3034 all_surface_verts_co.append(pt_co)
3036 # Define verts for each face
3037 all_surface_faces = []
3038 for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
3039 if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
3040 all_surface_faces.append(
3041 [i + 1, i, i + len(surface_splines_parsed[0]),
3042 i + len(surface_splines_parsed[0]) + 1]
3044 # Build the mesh
3045 surf_me_name = "SURFSKIO_surface"
3046 me_surf = bpy.data.meshes.new(surf_me_name)
3047 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
3048 ob_surface = object_utils.object_data_add(context, me_surf)
3050 # Select all the "unselected but participating" verts, from closed selection
3051 # or double selections with middle-vertex, for later join with remove doubles
3052 for v_idx in single_unselected_verts:
3053 self.main_object.data.vertices[v_idx].select = True
3055 # Join the new mesh to the main object
3056 ob_surface.select_set(True)
3057 self.main_object.select_set(True)
3058 bpy.context.view_layer.objects.active = self.main_object
3060 bpy.ops.object.join('INVOKE_REGION_WIN')
3062 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3064 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
3065 bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
3066 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
3068 self.update()
3070 return{'FINISHED'}
3072 def update(self):
3073 try:
3074 global global_offset
3075 shrinkwrap = self.main_object.modifiers["Shrinkwrap"]
3076 shrinkwrap.offset = global_offset
3077 bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset = global_offset
3078 except:
3079 pass
3081 try:
3082 global global_color
3083 material = makeMaterial("BSurfaceMesh", global_color)
3084 if self.main_object.data.materials:
3085 self.main_object.data.materials[0] = material
3086 else:
3087 self.main_object.data.materials.append(material)
3088 bpy.context.scene.bsurfaces.SURFSK_mesh_color = global_color
3089 except:
3090 pass
3092 try:
3093 global global_in_front
3094 self.main_object.show_in_front = global_in_front
3095 bpy.context.scene.bsurfaces.SURFSK_in_front = global_in_front
3096 except:
3097 pass
3099 try:
3100 global global_show_wire
3101 self.main_object.show_wire = global_show_wire
3102 bpy.context.scene.bsurfaces.SURFSK_show_wire = global_show_wire
3103 except:
3104 pass
3106 try:
3107 global global_shade_smooth
3108 if global_shade_smooth:
3109 bpy.ops.object.shade_smooth()
3110 else:
3111 bpy.ops.object.shade_flat()
3112 bpy.context.scene.bsurfaces.SURFSK_shade_smooth = global_shade_smooth
3113 except:
3114 pass
3116 return{'FINISHED'}
3118 def execute(self, context):
3120 if bpy.ops.object.mode_set.poll():
3121 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3123 try:
3124 global global_mesh_object
3125 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3126 bpy.data.objects[global_mesh_object].select_set(True)
3127 self.main_object = bpy.data.objects[global_mesh_object]
3128 bpy.context.view_layer.objects.active = self.main_object
3129 bsurfaces_props = bpy.context.scene.bsurfaces
3130 except:
3131 self.report({'WARNING'}, "Specify the name of the object with retopology")
3132 return{"CANCELLED"}
3133 bpy.context.view_layer.objects.active = self.main_object
3135 self.update()
3137 if not self.is_fill_faces:
3138 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3139 value='True, False, False')
3141 # Build splines from the "last saved splines".
3142 last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
3143 self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
3144 bpy.context.collection.objects.link(self.main_splines)
3146 last_saved_curve.dimensions = "3D"
3148 for sp in self.last_strokes_splines_coords:
3149 spline = self.main_splines.data.splines.new('BEZIER')
3150 # less one because one point is added when the spline is created
3151 spline.bezier_points.add(len(sp) - 1)
3152 for p in range(0, len(sp)):
3153 spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
3155 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3157 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3158 self.main_splines.select_set(True)
3159 bpy.context.view_layer.objects.active = self.main_splines
3161 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3163 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3164 # Important to make it vector first and then automatic, otherwise the
3165 # tips handles get too big and distort the shrinkwrap results later
3166 bpy.ops.curve.handle_type_set(type='VECTOR')
3167 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3168 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3169 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3171 self.main_splines.name = "SURFSKIO_temp_strokes"
3173 if self.is_crosshatch:
3174 strokes_for_crosshatch = True
3175 strokes_for_rectangular_surface = False
3176 else:
3177 strokes_for_rectangular_surface = True
3178 strokes_for_crosshatch = False
3180 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3182 if strokes_for_rectangular_surface:
3183 self.rectangular_surface(context)
3184 elif strokes_for_crosshatch:
3185 self.crosshatch_surface_execute(context)
3187 #Set Shade smooth to new polygons
3188 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3189 global global_shade_smooth
3190 if global_shade_smooth:
3191 bpy.ops.object.shade_smooth()
3192 else:
3193 bpy.ops.object.shade_flat()
3195 # Delete main splines
3196 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3197 if self.keep_strokes:
3198 self.main_splines.name = "keep_strokes"
3199 self.main_splines.data.bevel_depth = 0.001
3200 if "keep_strokes_material" in bpy.data.materials :
3201 self.main_splines.data.materials.append(bpy.data.materials["keep_strokes_material"])
3202 else:
3203 mat = bpy.data.materials.new("keep_strokes_material")
3204 mat.diffuse_color = (1, 0, 0, 0)
3205 mat.specular_color = (1, 0, 0)
3206 mat.specular_intensity = 0.0
3207 mat.roughness = 0.0
3208 self.main_splines.data.materials.append(mat)
3209 else:
3210 bpy.ops.object.delete({"selected_objects": [self.main_splines]})
3212 # Delete grease pencil strokes
3213 if self.strokes_type == "GP_STROKES" and not self.stopping_errors:
3214 try:
3215 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3216 except:
3217 pass
3219 # Delete annotations
3220 if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors:
3221 try:
3222 bpy.context.annotation_data.layers.active.clear()
3223 except:
3224 pass
3226 bsurfaces_props = bpy.context.scene.bsurfaces
3227 bsurfaces_props.SURFSK_edges_U = self.edges_U
3228 bsurfaces_props.SURFSK_edges_V = self.edges_V
3229 bsurfaces_props.SURFSK_cyclic_cross = self.cyclic_cross
3230 bsurfaces_props.SURFSK_cyclic_follow = self.cyclic_follow
3231 bsurfaces_props.SURFSK_automatic_join = self.automatic_join
3232 bsurfaces_props.SURFSK_loops_on_strokes = self.loops_on_strokes
3233 bsurfaces_props.SURFSK_keep_strokes = self.keep_strokes
3235 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3236 self.main_object.select_set(True)
3237 bpy.context.view_layer.objects.active = self.main_object
3239 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3241 self.update()
3243 return{'FINISHED'}
3245 def invoke(self, context, event):
3247 if bpy.ops.object.mode_set.poll():
3248 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3250 bsurfaces_props = bpy.context.scene.bsurfaces
3251 self.cyclic_cross = bsurfaces_props.SURFSK_cyclic_cross
3252 self.cyclic_follow = bsurfaces_props.SURFSK_cyclic_follow
3253 self.automatic_join = bsurfaces_props.SURFSK_automatic_join
3254 self.loops_on_strokes = bsurfaces_props.SURFSK_loops_on_strokes
3255 self.keep_strokes = bsurfaces_props.SURFSK_keep_strokes
3257 try:
3258 global global_mesh_object
3259 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3260 bpy.data.objects[global_mesh_object].select_set(True)
3261 self.main_object = bpy.data.objects[global_mesh_object]
3262 bpy.context.view_layer.objects.active = self.main_object
3263 except:
3264 self.report({'WARNING'}, "Specify the name of the object with retopology")
3265 return{"CANCELLED"}
3267 self.update()
3269 self.main_object_selected_verts_count = len([v for v in self.main_object.data.vertices if v.select])
3271 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3272 value='True, False, False')
3274 self.edges_U = bsurfaces_props.SURFSK_edges_U
3275 self.edges_V = bsurfaces_props.SURFSK_edges_V
3277 self.is_fill_faces = False
3278 self.stopping_errors = False
3279 self.last_strokes_splines_coords = []
3281 # Determine the type of the strokes
3282 self.strokes_type = get_strokes_type(context)
3284 # Check if it will be used grease pencil strokes or curves
3285 # If there are strokes to be used
3286 if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "GP_ANNOTATION":
3287 if self.strokes_type == "GP_STROKES":
3288 # Convert grease pencil strokes to curve
3289 global global_gpencil_object
3290 gp = bpy.data.objects[global_gpencil_object]
3291 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3292 self.using_external_curves = False
3294 elif self.strokes_type == "GP_ANNOTATION":
3295 # Convert grease pencil strokes to curve
3296 gp = bpy.context.annotation_data
3297 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'Annotation')
3298 self.using_external_curves = False
3300 elif self.strokes_type == "EXTERNAL_CURVE":
3301 global global_curve_object
3302 self.original_curve = bpy.data.objects[global_curve_object]
3303 self.using_external_curves = True
3305 # Make sure there are no objects left from erroneous
3306 # executions of this operator, with the reserved names used here
3307 for o in bpy.data.objects:
3308 if o.name.find("SURFSKIO_") != -1:
3309 bpy.ops.object.delete({"selected_objects": [o]})
3311 bpy.context.view_layer.objects.active = self.original_curve
3313 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3315 self.temporary_curve = bpy.context.view_layer.objects.active
3317 # Deselect all points of the curve
3318 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3319 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3320 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3322 # Delete splines with only a single isolated point
3323 for i in range(len(self.temporary_curve.data.splines)):
3324 sp = self.temporary_curve.data.splines[i]
3326 if len(sp.bezier_points) == 1:
3327 sp.bezier_points[0].select_control_point = True
3329 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3330 bpy.ops.curve.delete(type='VERT')
3331 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3333 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3334 self.temporary_curve.select_set(True)
3335 bpy.context.view_layer.objects.active = self.temporary_curve
3337 # Set a minimum number of points for crosshatch
3338 minimum_points_num = 15
3340 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3341 # Check if the number of points of each curve has at least the number of points
3342 # of minimum_points_num, which is a bit more than the face-loops limit.
3343 # If not, subdivide to reach at least that number of points
3344 for i in range(len(self.temporary_curve.data.splines)):
3345 sp = self.temporary_curve.data.splines[i]
3347 if len(sp.bezier_points) < minimum_points_num:
3348 for bp in sp.bezier_points:
3349 bp.select_control_point = True
3351 if (len(sp.bezier_points) - 1) != 0:
3352 # Formula to get the number of cuts that will make a curve
3353 # of N number of points have near to "minimum_points_num"
3354 # points, when subdividing with this number of cuts
3355 subdivide_cuts = int(
3356 (minimum_points_num - len(sp.bezier_points)) /
3357 (len(sp.bezier_points) - 1)
3358 ) + 1
3359 else:
3360 subdivide_cuts = 0
3362 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3363 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3365 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3367 # Detect if the strokes are a crosshatch and do it if it is
3368 self.crosshatch_surface_invoke(self.temporary_curve)
3370 if not self.is_crosshatch:
3371 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3372 self.temporary_curve.select_set(True)
3373 bpy.context.view_layer.objects.active = self.temporary_curve
3375 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3377 # Set a minimum number of points for rectangular surfaces
3378 minimum_points_num = 60
3380 # Check if the number of points of each curve has at least the number of points
3381 # of minimum_points_num, which is a bit more than the face-loops limit.
3382 # If not, subdivide to reach at least that number of points
3383 for i in range(len(self.temporary_curve.data.splines)):
3384 sp = self.temporary_curve.data.splines[i]
3386 if len(sp.bezier_points) < minimum_points_num:
3387 for bp in sp.bezier_points:
3388 bp.select_control_point = True
3390 if (len(sp.bezier_points) - 1) != 0:
3391 # Formula to get the number of cuts that will make a curve of
3392 # N number of points have near to "minimum_points_num" points,
3393 # when subdividing with this number of cuts
3394 subdivide_cuts = int(
3395 (minimum_points_num - len(sp.bezier_points)) /
3396 (len(sp.bezier_points) - 1)
3397 ) + 1
3398 else:
3399 subdivide_cuts = 0
3401 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3402 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3404 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3406 # Save coordinates of the actual strokes (as the "last saved splines")
3407 for sp_idx in range(len(self.temporary_curve.data.splines)):
3408 self.last_strokes_splines_coords.append([])
3409 for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
3410 coords = self.temporary_curve.matrix_world @ \
3411 self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
3412 self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
3414 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3415 for sp_idx in range(len(self.temporary_curve.data.splines)):
3416 if self.temporary_curve.data.splines[sp_idx].use_cyclic_u is True:
3417 first_p_co = self.last_strokes_splines_coords[sp_idx][0]
3418 last_p_co = self.last_strokes_splines_coords[sp_idx][
3419 len(self.last_strokes_splines_coords[sp_idx]) - 1
3421 target_co = [
3422 (first_p_co[0] + last_p_co[0]) / 2,
3423 (first_p_co[1] + last_p_co[1]) / 2,
3424 (first_p_co[2] + last_p_co[2]) / 2
3427 self.last_strokes_splines_coords[sp_idx][0] = target_co
3428 self.last_strokes_splines_coords[sp_idx][
3429 len(self.last_strokes_splines_coords[sp_idx]) - 1
3430 ] = target_co
3431 tuple(self.last_strokes_splines_coords)
3433 # Estimation of the average length of the segments between
3434 # each point of the grease pencil strokes.
3435 # Will be useful to determine whether a curve should be made "Cyclic"
3436 segments_lengths_sum = 0
3437 segments_count = 0
3438 random_spline = self.temporary_curve.data.splines[0].bezier_points
3439 for i in range(0, len(random_spline)):
3440 if i != 0 and len(random_spline) - 1 >= i:
3441 segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
3442 segments_count += 1
3444 self.average_gp_segment_length = segments_lengths_sum / segments_count
3446 # Delete temporary strokes curve object
3447 bpy.ops.object.delete({"selected_objects": [self.temporary_curve]})
3449 # Set again since "execute()" will turn it again to its initial value
3450 self.execute(context)
3452 if not self.stopping_errors:
3453 # Delete grease pencil strokes
3454 if self.strokes_type == "GP_STROKES":
3455 try:
3456 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3457 except:
3458 pass
3460 # Delete annotation strokes
3461 elif self.strokes_type == "GP_ANNOTATION":
3462 try:
3463 bpy.context.annotation_data.layers.active.clear()
3464 except:
3465 pass
3467 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3468 bpy.ops.object.delete({"selected_objects": [self.original_curve]})
3469 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3471 return {"FINISHED"}
3472 else:
3473 return{"CANCELLED"}
3475 elif self.strokes_type == "SELECTION_ALONE":
3476 self.is_fill_faces = True
3477 created_faces_count = self.fill_with_faces(self.main_object)
3479 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3481 if created_faces_count == 0:
3482 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3483 return {"CANCELLED"}
3484 else:
3485 return {"FINISHED"}
3487 if self.strokes_type == "EXTERNAL_NO_CURVE":
3488 self.report({'WARNING'}, "The secondary object is not a Curve.")
3489 return{"CANCELLED"}
3491 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3492 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3493 return{"CANCELLED"}
3495 elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or \
3496 self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3498 self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3499 return{"CANCELLED"}
3501 elif self.strokes_type == "NO_STROKES":
3502 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3503 return{"CANCELLED"}
3505 elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
3506 self.report({'WARNING'}, "All splines must be Bezier.")
3507 return{"CANCELLED"}
3509 else:
3510 return{"CANCELLED"}
3512 # ----------------------------
3513 # Init operator
3514 class MESH_OT_SURFSK_init(Operator):
3515 bl_idname = "mesh.surfsk_init"
3516 bl_label = "Bsurfaces initialize"
3517 bl_description = "Add an empty mesh object with useful settings"
3518 bl_options = {'REGISTER', 'UNDO'}
3520 def execute(self, context):
3522 bs = bpy.context.scene.bsurfaces
3524 if bpy.ops.object.mode_set.poll():
3525 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3527 global global_color
3528 global global_offset
3529 global global_in_front
3530 global global_show_wire
3531 global global_shade_smooth
3532 global global_mesh_object
3533 global global_gpencil_object
3535 if bs.SURFSK_mesh == None:
3536 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3537 mesh = bpy.data.meshes.new('BSurfaceMesh')
3538 mesh_object = object_utils.object_data_add(context, mesh)
3539 mesh_object.select_set(True)
3540 bpy.context.view_layer.objects.active = mesh_object
3542 mesh_object.show_all_edges = True
3543 global_in_front = bpy.context.scene.bsurfaces.SURFSK_in_front
3544 mesh_object.show_in_front = global_in_front
3545 mesh_object.display_type = 'SOLID'
3546 mesh_object.show_wire = True
3548 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
3549 if global_shade_smooth:
3550 bpy.ops.object.shade_smooth()
3551 else:
3552 bpy.ops.object.shade_flat()
3554 global_show_wire = bpy.context.scene.bsurfaces.SURFSK_show_wire
3555 mesh_object.show_wire = global_show_wire
3557 global_color = bpy.context.scene.bsurfaces.SURFSK_mesh_color
3558 material = makeMaterial("BSurfaceMesh", global_color)
3559 mesh_object.data.materials.append(material)
3560 bpy.ops.object.modifier_add(type='SHRINKWRAP')
3561 modifier = mesh_object.modifiers["Shrinkwrap"]
3562 if self.active_object is not None:
3563 modifier.target = self.active_object
3564 modifier.wrap_method = 'TARGET_PROJECT'
3565 modifier.wrap_mode = 'OUTSIDE_SURFACE'
3566 modifier.show_on_cage = True
3567 global_offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3568 modifier.offset = global_offset
3570 global_mesh_object = mesh_object.name
3571 bpy.context.scene.bsurfaces.SURFSK_mesh = bpy.data.objects[global_mesh_object]
3573 bpy.context.scene.tool_settings.snap_elements = {'FACE'}
3574 bpy.context.scene.tool_settings.use_snap = True
3575 bpy.context.scene.tool_settings.use_snap_self = False
3576 bpy.context.scene.tool_settings.use_snap_align_rotation = True
3577 bpy.context.scene.tool_settings.use_snap_project = True
3578 bpy.context.scene.tool_settings.use_snap_rotate = True
3579 bpy.context.scene.tool_settings.use_snap_scale = True
3581 bpy.context.scene.tool_settings.use_mesh_automerge = True
3582 bpy.context.scene.tool_settings.double_threshold = 0.01
3584 if context.scene.bsurfaces.SURFSK_guide == 'GPencil' and bs.SURFSK_gpencil == None:
3585 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3586 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')
3587 bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d = 'SURFACE'
3588 gpencil_object = bpy.context.scene.objects[bpy.context.scene.objects[-1].name]
3589 gpencil_object.select_set(True)
3590 bpy.context.view_layer.objects.active = gpencil_object
3591 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3592 global_gpencil_object = gpencil_object.name
3593 bpy.context.scene.bsurfaces.SURFSK_gpencil = bpy.data.objects[global_gpencil_object]
3594 gpencil_object.data.stroke_depth_order = '3D'
3595 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3596 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3598 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
3599 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3600 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3602 def invoke(self, context, event):
3603 if bpy.context.active_object:
3604 self.active_object = bpy.context.active_object
3605 else:
3606 self.active_object = None
3608 self.execute(context)
3610 return {"FINISHED"}
3612 # ----------------------------
3613 # Add modifiers operator
3614 class MESH_OT_SURFSK_add_modifiers(Operator):
3615 bl_idname = "mesh.surfsk_add_modifiers"
3616 bl_label = "Add Mirror and others modifiers"
3617 bl_description = "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3618 bl_options = {'REGISTER', 'UNDO'}
3620 def execute(self, context):
3622 bs = bpy.context.scene.bsurfaces
3624 if bpy.ops.object.mode_set.poll():
3625 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3627 if bs.SURFSK_mesh == None:
3628 self.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3629 else:
3630 mesh_object = bs.SURFSK_mesh
3632 try:
3633 mesh_object.select_set(True)
3634 except:
3635 self.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3636 return {"CANCEL"}
3638 bpy.context.view_layer.objects.active = mesh_object
3640 try:
3641 shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
3642 if self.active_object is not None and self.active_object != mesh_object:
3643 shrinkwrap.target = self.active_object
3644 shrinkwrap.wrap_method = 'TARGET_PROJECT'
3645 shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
3646 shrinkwrap.show_on_cage = True
3647 shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3648 except:
3649 bpy.ops.object.modifier_add(type='SHRINKWRAP')
3650 shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
3651 if self.active_object is not None and self.active_object != mesh_object:
3652 shrinkwrap.target = self.active_object
3653 shrinkwrap.wrap_method = 'TARGET_PROJECT'
3654 shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
3655 shrinkwrap.show_on_cage = True
3656 shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3658 try:
3659 mirror = mesh_object.modifiers["Mirror"]
3660 mirror.use_clip = True
3661 except:
3662 bpy.ops.object.modifier_add(type='MIRROR')
3663 mirror = mesh_object.modifiers["Mirror"]
3664 mirror.use_clip = True
3666 try:
3667 _subsurf = mesh_object.modifiers["Subdivision"]
3668 except:
3669 bpy.ops.object.modifier_add(type='SUBSURF')
3670 _subsurf = mesh_object.modifiers["Subdivision"]
3672 try:
3673 solidify = mesh_object.modifiers["Solidify"]
3674 solidify.thickness = 0.01
3675 except:
3676 bpy.ops.object.modifier_add(type='SOLIDIFY')
3677 solidify = mesh_object.modifiers["Solidify"]
3678 solidify.thickness = 0.01
3680 return {"FINISHED"}
3682 def invoke(self, context, event):
3683 if bpy.context.active_object:
3684 self.active_object = bpy.context.active_object
3685 else:
3686 self.active_object = None
3688 self.execute(context)
3690 return {"FINISHED"}
3692 # ----------------------------
3693 # Edit surface operator
3694 class MESH_OT_SURFSK_edit_surface(Operator):
3695 bl_idname = "mesh.surfsk_edit_surface"
3696 bl_label = "Bsurfaces edit surface"
3697 bl_description = "Edit surface mesh"
3698 bl_options = {'REGISTER', 'UNDO'}
3700 def execute(self, context):
3701 if bpy.ops.object.mode_set.poll():
3702 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3703 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3704 bpy.context.scene.bsurfaces.SURFSK_mesh.select_set(True)
3705 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_mesh
3706 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3707 bpy.ops.wm.tool_set_by_id(name="builtin.select")
3709 def invoke(self, context, event):
3710 try:
3711 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3712 bpy.data.objects[global_mesh_object].select_set(True)
3713 self.main_object = bpy.data.objects[global_mesh_object]
3714 bpy.context.view_layer.objects.active = self.main_object
3715 except:
3716 self.report({'WARNING'}, "Specify the name of the object with retopology")
3717 return{"CANCELLED"}
3719 self.execute(context)
3721 return {"FINISHED"}
3723 # ----------------------------
3724 # Add strokes operator
3725 class GPENCIL_OT_SURFSK_add_strokes(Operator):
3726 bl_idname = "gpencil.surfsk_add_strokes"
3727 bl_label = "Bsurfaces add strokes"
3728 bl_description = "Add the grease pencil strokes"
3729 bl_options = {'REGISTER', 'UNDO'}
3731 def execute(self, context):
3732 if bpy.ops.object.mode_set.poll():
3733 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3734 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3736 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3737 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_gpencil
3738 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3739 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3741 return{"FINISHED"}
3743 def invoke(self, context, event):
3744 try:
3745 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3746 except:
3747 self.report({'WARNING'}, "Specify the name of the object with strokes")
3748 return{"CANCELLED"}
3750 self.execute(context)
3752 return {"FINISHED"}
3754 # ----------------------------
3755 # Edit strokes operator
3756 class GPENCIL_OT_SURFSK_edit_strokes(Operator):
3757 bl_idname = "gpencil.surfsk_edit_strokes"
3758 bl_label = "Bsurfaces edit strokes"
3759 bl_description = "Edit the grease pencil strokes"
3760 bl_options = {'REGISTER', 'UNDO'}
3762 def execute(self, context):
3763 if bpy.ops.object.mode_set.poll():
3764 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3765 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3767 gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil
3769 gpencil_object.select_set(True)
3770 bpy.context.view_layer.objects.active = gpencil_object
3772 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT_GPENCIL')
3773 try:
3774 bpy.ops.gpencil.select_all(action='SELECT')
3775 except:
3776 pass
3778 def invoke(self, context, event):
3779 try:
3780 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3781 except:
3782 self.report({'WARNING'}, "Specify the name of the object with strokes")
3783 return{"CANCELLED"}
3785 self.execute(context)
3787 return {"FINISHED"}
3789 # ----------------------------
3790 # Convert annotation to curves operator
3791 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator):
3792 bl_idname = "gpencil.surfsk_annotations_to_curves"
3793 bl_label = "Convert annotation to curves"
3794 bl_description = "Convert annotation to curves for editing"
3795 bl_options = {'REGISTER', 'UNDO'}
3797 def execute(self, context):
3799 if bpy.ops.object.mode_set.poll():
3800 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3802 # Convert annotation to curve
3803 curve = conver_gpencil_to_curve(self, context, None, 'Annotation')
3805 if curve != None:
3806 # Delete annotation strokes
3807 try:
3808 bpy.context.annotation_data.layers.active.clear()
3809 except:
3810 pass
3812 # Clean up curves
3813 curve.select_set(True)
3814 bpy.context.view_layer.objects.active = curve
3816 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3818 return {"FINISHED"}
3820 def invoke(self, context, event):
3821 try:
3822 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
3824 _strokes_num = len(strokes)
3825 except:
3826 self.report({'WARNING'}, "Not active annotation")
3827 return{"CANCELLED"}
3829 self.execute(context)
3831 return {"FINISHED"}
3833 # ----------------------------
3834 # Convert strokes to curves operator
3835 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator):
3836 bl_idname = "gpencil.surfsk_strokes_to_curves"
3837 bl_label = "Convert strokes to curves"
3838 bl_description = "Convert grease pencil strokes to curves for editing"
3839 bl_options = {'REGISTER', 'UNDO'}
3841 def execute(self, context):
3843 if bpy.ops.object.mode_set.poll():
3844 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3846 # Convert grease pencil strokes to curve
3847 gp = bpy.context.scene.bsurfaces.SURFSK_gpencil
3848 curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3850 if curve != None:
3851 # Delete grease pencil strokes
3852 try:
3853 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3854 except:
3855 pass
3857 # Clean up curves
3859 curve.select_set(True)
3860 bpy.context.view_layer.objects.active = curve
3862 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3864 return {"FINISHED"}
3866 def invoke(self, context, event):
3867 try:
3868 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3869 except:
3870 self.report({'WARNING'}, "Specify the name of the object with strokes")
3871 return{"CANCELLED"}
3873 self.execute(context)
3875 return {"FINISHED"}
3877 # ----------------------------
3878 # Add annotation
3879 class GPENCIL_OT_SURFSK_add_annotation(Operator):
3880 bl_idname = "gpencil.surfsk_add_annotation"
3881 bl_label = "Bsurfaces add annotation"
3882 bl_description = "Add annotation"
3883 bl_options = {'REGISTER', 'UNDO'}
3885 def execute(self, context):
3886 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3887 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3889 return{"FINISHED"}
3891 def invoke(self, context, event):
3893 self.execute(context)
3895 return {"FINISHED"}
3898 # ----------------------------
3899 # Edit curve operator
3900 class CURVE_OT_SURFSK_edit_curve(Operator):
3901 bl_idname = "curve.surfsk_edit_curve"
3902 bl_label = "Bsurfaces edit curve"
3903 bl_description = "Edit curve"
3904 bl_options = {'REGISTER', 'UNDO'}
3906 def execute(self, context):
3907 if bpy.ops.object.mode_set.poll():
3908 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3909 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3910 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3911 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_curve
3912 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3914 def invoke(self, context, event):
3915 try:
3916 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3917 except:
3918 self.report({'WARNING'}, "Specify the name of the object with curve")
3919 return{"CANCELLED"}
3921 self.execute(context)
3923 return {"FINISHED"}
3925 # ----------------------------
3926 # Reorder splines
3927 class CURVE_OT_SURFSK_reorder_splines(Operator):
3928 bl_idname = "curve.surfsk_reorder_splines"
3929 bl_label = "Bsurfaces reorder splines"
3930 bl_description = "Defines the order of the splines by using grease pencil strokes"
3931 bl_options = {'REGISTER', 'UNDO'}
3933 def execute(self, context):
3934 objects_to_delete = []
3935 # Convert grease pencil strokes to curve.
3936 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3937 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3938 for ob in bpy.context.selected_objects:
3939 if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
3940 GP_strokes_curve = ob
3942 # GP_strokes_curve = bpy.context.object
3943 objects_to_delete.append(GP_strokes_curve)
3945 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3946 GP_strokes_curve.select_set(True)
3947 bpy.context.view_layer.objects.active = GP_strokes_curve
3949 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3950 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3951 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=100)
3952 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3954 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3955 GP_strokes_mesh = bpy.context.object
3956 objects_to_delete.append(GP_strokes_mesh)
3958 GP_strokes_mesh.data.resolution_u = 1
3959 bpy.ops.object.convert(target='MESH', keep_original=False)
3961 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3962 self.main_curve.select_set(True)
3963 bpy.context.view_layer.objects.active = self.main_curve
3965 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3966 curves_duplicate_1 = bpy.context.object
3967 objects_to_delete.append(curves_duplicate_1)
3969 minimum_points_num = 500
3971 # Some iterations since the subdivision operator
3972 # has a limit of 100 subdivisions per iteration
3973 for x in range(round(minimum_points_num / 100)):
3974 # Check if the number of points of each curve has at least the number of points
3975 # of minimum_points_num. If not, subdivide to reach at least that number of points
3976 for i in range(len(curves_duplicate_1.data.splines)):
3977 sp = curves_duplicate_1.data.splines[i]
3979 if len(sp.bezier_points) < minimum_points_num:
3980 for bp in sp.bezier_points:
3981 bp.select_control_point = True
3983 if (len(sp.bezier_points) - 1) != 0:
3984 # Formula to get the number of cuts that will make a curve of N
3985 # number of points have near to "minimum_points_num" points,
3986 # when subdividing with this number of cuts
3987 subdivide_cuts = int(
3988 (minimum_points_num - len(sp.bezier_points)) /
3989 (len(sp.bezier_points) - 1)
3990 ) + 1
3991 else:
3992 subdivide_cuts = 0
3994 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3995 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3996 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3997 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3999 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
4000 curves_duplicate_2 = bpy.context.object
4001 objects_to_delete.append(curves_duplicate_2)
4003 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
4004 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4005 curves_duplicate_2.select_set(True)
4006 bpy.context.view_layer.objects.active = curves_duplicate_2
4008 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
4009 curves_duplicate_2.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX"
4010 curves_duplicate_2.modifiers["Shrinkwrap"].target = GP_strokes_mesh
4011 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier='Shrinkwrap')
4013 # Get the distance of each vert from its original position to its position with Shrinkwrap
4014 nearest_points_coords = {}
4015 for st_idx in range(len(curves_duplicate_1.data.splines)):
4016 for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
4017 bp_1_co = curves_duplicate_1.matrix_world @ \
4018 curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
4020 bp_2_co = curves_duplicate_2.matrix_world @ \
4021 curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
4023 if bp_idx == 0:
4024 shortest_dist = (bp_1_co - bp_2_co).length
4025 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
4026 "%.4f" % bp_2_co[1],
4027 "%.4f" % bp_2_co[2])
4029 dist = (bp_1_co - bp_2_co).length
4031 if dist < shortest_dist:
4032 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
4033 "%.4f" % bp_2_co[1],
4034 "%.4f" % bp_2_co[2])
4035 shortest_dist = dist
4037 # Get all coords of GP strokes points, for comparison
4038 GP_strokes_coords = []
4039 for st_idx in range(len(GP_strokes_curve.data.splines)):
4040 GP_strokes_coords.append(
4041 [("%.4f" % x if "%.4f" % x != "-0.00" else "0.00",
4042 "%.4f" % y if "%.4f" % y != "-0.00" else "0.00",
4043 "%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for
4044 x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]]
4047 # Check the point of the GP strokes with the same coords as
4048 # the nearest points of the curves (with shrinkwrap)
4050 # Dictionary with GP stroke index as index, and a list as value.
4051 # The list has as index the point index of the GP stroke
4052 # nearest to the spline, and as value the spline index
4053 GP_connection_points = {}
4054 for gp_st_idx in range(len(GP_strokes_coords)):
4055 GPvert_spline_relationship = {}
4057 for splines_st_idx in range(len(nearest_points_coords)):
4058 if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
4059 GPvert_spline_relationship[
4060 GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])
4061 ] = splines_st_idx
4063 GP_connection_points[gp_st_idx] = GPvert_spline_relationship
4065 # Get the splines new order
4066 splines_new_order = []
4067 for i in GP_connection_points:
4068 dict_keys = sorted(GP_connection_points[i].keys()) # Sort dictionaries by key
4070 for k in dict_keys:
4071 splines_new_order.append(GP_connection_points[i][k])
4073 # Reorder
4074 curve_original_name = self.main_curve.name
4076 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4077 self.main_curve.select_set(True)
4078 bpy.context.view_layer.objects.active = self.main_curve
4080 self.main_curve.name = "SURFSKIO_CRV_ORD"
4082 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4083 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4084 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4086 for _sp_idx in range(len(self.main_curve.data.splines)):
4087 self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
4089 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4090 bpy.ops.curve.separate('EXEC_REGION_WIN')
4091 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4093 # Get the names of the separated splines objects in the original order
4094 splines_unordered = {}
4095 for o in bpy.data.objects:
4096 if o.name.find("SURFSKIO_CRV_ORD") != -1:
4097 spline_order_string = o.name.partition(".")[2]
4099 if spline_order_string != "" and int(spline_order_string) > 0:
4100 spline_order_index = int(spline_order_string) - 1
4101 splines_unordered[spline_order_index] = o.name
4103 # Join all splines objects in final order
4104 for order_idx in splines_new_order:
4105 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4106 bpy.data.objects[splines_unordered[order_idx]].select_set(True)
4107 bpy.data.objects["SURFSKIO_CRV_ORD"].select_set(True)
4108 bpy.context.view_layer.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
4110 bpy.ops.object.join('INVOKE_REGION_WIN')
4112 # Go back to the original name of the curves object.
4113 bpy.context.object.name = curve_original_name
4115 # Delete all unused objects
4116 bpy.ops.object.delete({"selected_objects": objects_to_delete})
4118 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4119 bpy.data.objects[curve_original_name].select_set(True)
4120 bpy.context.view_layer.objects.active = bpy.data.objects[curve_original_name]
4122 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4123 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4125 try:
4126 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
4127 except:
4128 pass
4131 return {"FINISHED"}
4133 def invoke(self, context, event):
4134 self.main_curve = bpy.context.object
4135 there_are_GP_strokes = False
4137 try:
4138 # Get the active grease pencil layer
4139 strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
4141 if strokes_num > 0:
4142 there_are_GP_strokes = True
4143 except:
4144 pass
4146 if there_are_GP_strokes:
4147 self.execute(context)
4148 self.report({'INFO'}, "Splines have been reordered")
4149 else:
4150 self.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4152 return {"FINISHED"}
4154 # ----------------------------
4155 # Set first points operator
4156 class CURVE_OT_SURFSK_first_points(Operator):
4157 bl_idname = "curve.surfsk_first_points"
4158 bl_label = "Bsurfaces set first points"
4159 bl_description = "Set the selected points as the first point of each spline"
4160 bl_options = {'REGISTER', 'UNDO'}
4162 def execute(self, context):
4163 splines_to_invert = []
4165 # Check non-cyclic splines to invert
4166 for i in range(len(self.main_curve.data.splines)):
4167 b_points = self.main_curve.data.splines[i].bezier_points
4169 if i not in self.cyclic_splines: # Only for non-cyclic splines
4170 if b_points[len(b_points) - 1].select_control_point:
4171 splines_to_invert.append(i)
4173 # Reorder points of cyclic splines, and set all handles to "Automatic"
4175 # Check first selected point
4176 cyclic_splines_new_first_pt = {}
4177 for i in self.cyclic_splines:
4178 sp = self.main_curve.data.splines[i]
4180 for t in range(len(sp.bezier_points)):
4181 bp = sp.bezier_points[t]
4182 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
4183 cyclic_splines_new_first_pt[i] = t
4184 break # To take only one if there are more
4186 # Reorder
4187 for spline_idx in cyclic_splines_new_first_pt:
4188 sp = self.main_curve.data.splines[spline_idx]
4190 spline_old_coords = []
4191 for bp_old in sp.bezier_points:
4192 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
4194 left_handle_type = str(bp_old.handle_left_type)
4195 left_handle_length = float(bp_old.handle_left.length)
4196 left_handle_xyz = (
4197 float(bp_old.handle_left.x),
4198 float(bp_old.handle_left.y),
4199 float(bp_old.handle_left.z)
4201 right_handle_type = str(bp_old.handle_right_type)
4202 right_handle_length = float(bp_old.handle_right.length)
4203 right_handle_xyz = (
4204 float(bp_old.handle_right.x),
4205 float(bp_old.handle_right.y),
4206 float(bp_old.handle_right.z)
4208 spline_old_coords.append(
4209 [coords, left_handle_type,
4210 right_handle_type, left_handle_length,
4211 right_handle_length, left_handle_xyz,
4212 right_handle_xyz]
4215 for t in range(len(sp.bezier_points)):
4216 bp = sp.bezier_points
4218 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
4219 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
4220 else:
4221 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
4223 bp[t].co = Vector(spline_old_coords[new_index][0])
4225 bp[t].handle_left.length = spline_old_coords[new_index][3]
4226 bp[t].handle_right.length = spline_old_coords[new_index][4]
4228 bp[t].handle_left_type = "FREE"
4229 bp[t].handle_right_type = "FREE"
4231 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
4232 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
4233 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
4235 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
4236 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
4237 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
4239 bp[t].handle_left_type = spline_old_coords[new_index][1]
4240 bp[t].handle_right_type = spline_old_coords[new_index][2]
4242 # Invert the non-cyclic splines designated above
4243 for i in range(len(splines_to_invert)):
4244 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4246 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4247 self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
4248 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4250 bpy.ops.curve.switch_direction()
4252 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4254 # Keep selected the first vert of each spline
4255 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4256 for i in range(len(self.main_curve.data.splines)):
4257 if not self.main_curve.data.splines[i].use_cyclic_u:
4258 bp = self.main_curve.data.splines[i].bezier_points[0]
4259 else:
4260 bp = self.main_curve.data.splines[i].bezier_points[
4261 len(self.main_curve.data.splines[i].bezier_points) - 1
4264 bp.select_control_point = True
4265 bp.select_right_handle = True
4266 bp.select_left_handle = True
4268 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4270 return {'FINISHED'}
4272 def invoke(self, context, event):
4273 self.main_curve = bpy.context.object
4275 # Check if all curves are Bezier, and detect which ones are cyclic
4276 self.cyclic_splines = []
4277 for i in range(len(self.main_curve.data.splines)):
4278 if self.main_curve.data.splines[i].type != "BEZIER":
4279 self.report({'WARNING'}, "All splines must be Bezier type")
4281 return {'CANCELLED'}
4282 else:
4283 if self.main_curve.data.splines[i].use_cyclic_u:
4284 self.cyclic_splines.append(i)
4286 self.execute(context)
4287 self.report({'INFO'}, "First points have been set")
4289 return {'FINISHED'}
4292 # Add-ons Preferences Update Panel
4294 # Define Panel classes for updating
4295 panels = (
4296 VIEW3D_PT_tools_SURFSK_mesh,
4297 VIEW3D_PT_tools_SURFSK_curve
4301 def conver_gpencil_to_curve(self, context, pencil, type):
4302 newCurve = bpy.data.curves.new(type + '_curve', type='CURVE')
4303 newCurve.dimensions = '3D'
4304 CurveObject = object_utils.object_data_add(context, newCurve)
4305 error = False
4307 if type == 'GPensil':
4308 try:
4309 strokes = pencil.data.layers.active.active_frame.strokes
4310 except:
4311 error = True
4312 CurveObject.location = pencil.location
4313 CurveObject.rotation_euler = pencil.rotation_euler
4314 CurveObject.scale = pencil.scale
4315 elif type == 'Annotation':
4316 try:
4317 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
4318 except:
4319 error = True
4320 CurveObject.location = (0.0, 0.0, 0.0)
4321 CurveObject.rotation_euler = (0.0, 0.0, 0.0)
4322 CurveObject.scale = (1.0, 1.0, 1.0)
4324 if not error:
4325 for i, _stroke in enumerate(strokes):
4326 stroke_points = strokes[i].points
4327 data_list = [ (point.co.x, point.co.y, point.co.z)
4328 for point in stroke_points ]
4329 points_to_add = len(data_list)-1
4331 flat_list = []
4332 for point in data_list:
4333 flat_list.extend(point)
4335 spline = newCurve.splines.new(type='BEZIER')
4336 spline.bezier_points.add(points_to_add)
4337 spline.bezier_points.foreach_set("co", flat_list)
4339 for point in spline.bezier_points:
4340 point.handle_left_type="AUTO"
4341 point.handle_right_type="AUTO"
4343 return CurveObject
4344 else:
4345 return None
4348 def update_panel(self, context):
4349 message = "Bsurfaces GPL Edition: Updating Panel locations has failed"
4350 try:
4351 for panel in panels:
4352 if "bl_rna" in panel.__dict__:
4353 bpy.utils.unregister_class(panel)
4355 for panel in panels:
4356 category = context.preferences.addons[__name__].preferences.category
4357 if category != 'Tool':
4358 panel.bl_category = context.preferences.addons[__name__].preferences.category
4359 else:
4360 context.preferences.addons[__name__].preferences.category = 'Edit'
4361 panel.bl_category = 'Edit'
4362 raise ValueError("You can not install add-ons in the Tool panel")
4363 bpy.utils.register_class(panel)
4365 except Exception as e:
4366 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
4367 pass
4369 def makeMaterial(name, diffuse):
4371 if name in bpy.data.materials:
4372 material = bpy.data.materials[name]
4373 material.diffuse_color = diffuse
4374 else:
4375 material = bpy.data.materials.new(name)
4376 material.diffuse_color = diffuse
4378 return material
4380 def update_mesh(self, context):
4381 try:
4382 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4383 bpy.ops.object.select_all(action='DESELECT')
4384 bpy.context.view_layer.update()
4385 global global_mesh_object
4386 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4387 bpy.data.objects[global_mesh_object].select_set(True)
4388 bpy.context.view_layer.objects.active = bpy.data.objects[global_mesh_object]
4389 except:
4390 print("Select mesh object")
4392 def update_gpencil(self, context):
4393 try:
4394 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4395 bpy.ops.object.select_all(action='DESELECT')
4396 bpy.context.view_layer.update()
4397 global global_gpencil_object
4398 global_gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil.name
4399 bpy.data.objects[global_gpencil_object].select_set(True)
4400 bpy.context.view_layer.objects.active = bpy.data.objects[global_gpencil_object]
4401 except:
4402 print("Select gpencil object")
4404 def update_curve(self, context):
4405 try:
4406 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4407 bpy.ops.object.select_all(action='DESELECT')
4408 bpy.context.view_layer.update()
4409 global global_curve_object
4410 global_curve_object = bpy.context.scene.bsurfaces.SURFSK_curve.name
4411 bpy.data.objects[global_curve_object].select_set(True)
4412 bpy.context.view_layer.objects.active = bpy.data.objects[global_curve_object]
4413 except:
4414 print("Select curve object")
4416 def update_color(self, context):
4417 try:
4418 global global_color
4419 global global_mesh_object
4420 material = makeMaterial("BSurfaceMesh", bpy.context.scene.bsurfaces.SURFSK_mesh_color)
4421 if bpy.data.objects[global_mesh_object].data.materials:
4422 bpy.data.objects[global_mesh_object].data.materials[0] = material
4423 else:
4424 bpy.data.objects[global_mesh_object].data.materials.append(material)
4425 diffuse_color = material.diffuse_color
4426 global_color = (diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3])
4427 except:
4428 print("Select mesh object")
4430 def update_Shrinkwrap_offset(self, context):
4431 try:
4432 global global_offset
4433 global_offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
4434 global global_mesh_object
4435 modifier = bpy.data.objects[global_mesh_object].modifiers["Shrinkwrap"]
4436 modifier.offset = global_offset
4437 except:
4438 print("Shrinkwrap modifier not found")
4440 def update_in_front(self, context):
4441 try:
4442 global global_in_front
4443 global_in_front = bpy.context.scene.bsurfaces.SURFSK_in_front
4444 global global_mesh_object
4445 bpy.data.objects[global_mesh_object].show_in_front = global_in_front
4446 except:
4447 print("Select mesh object")
4449 def update_show_wire(self, context):
4450 try:
4451 global global_show_wire
4452 global_show_wire = bpy.context.scene.bsurfaces.SURFSK_show_wire
4453 global global_mesh_object
4454 bpy.data.objects[global_mesh_object].show_wire = global_show_wire
4455 except:
4456 print("Select mesh object")
4458 def update_shade_smooth(self, context):
4459 try:
4460 global global_shade_smooth
4461 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
4463 contex_mode = bpy.context.mode
4465 if bpy.ops.object.mode_set.poll():
4466 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4468 bpy.ops.object.select_all(action='DESELECT')
4469 global global_mesh_object
4470 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4471 bpy.data.objects[global_mesh_object].select_set(True)
4473 if global_shade_smooth:
4474 bpy.ops.object.shade_smooth()
4475 else:
4476 bpy.ops.object.shade_flat()
4478 if contex_mode == "EDIT_MESH":
4479 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4481 except:
4482 print("Select mesh object")
4485 class BsurfPreferences(AddonPreferences):
4486 # this must match the addon name, use '__package__'
4487 # when defining this in a submodule of a python package.
4488 bl_idname = __name__
4490 category: StringProperty(
4491 name="Tab Category",
4492 description="Choose a name for the category of the panel",
4493 default="Edit",
4494 update=update_panel
4497 def draw(self, context):
4498 layout = self.layout
4500 row = layout.row()
4501 col = row.column()
4502 col.label(text="Tab Category:")
4503 col.prop(self, "category", text="")
4505 # Properties
4506 class BsurfacesProps(PropertyGroup):
4507 SURFSK_guide: EnumProperty(
4508 name="Guide:",
4509 items=[
4510 ('Annotation', 'Annotation', 'Annotation'),
4511 ('GPencil', 'GPencil', 'GPencil'),
4512 ('Curve', 'Curve', 'Curve')
4514 default="Annotation"
4516 SURFSK_edges_U: IntProperty(
4517 name="Cross",
4518 description="Number of face-loops crossing the strokes",
4519 default=5,
4520 min=1,
4521 max=200
4523 SURFSK_edges_V: IntProperty(
4524 name="Follow",
4525 description="Number of face-loops following the strokes",
4526 default=1,
4527 min=1,
4528 max=200
4530 SURFSK_cyclic_cross: BoolProperty(
4531 name="Cyclic Cross",
4532 description="Make cyclic the face-loops crossing the strokes",
4533 default=False
4535 SURFSK_cyclic_follow: BoolProperty(
4536 name="Cyclic Follow",
4537 description="Make cyclic the face-loops following the strokes",
4538 default=False
4540 SURFSK_keep_strokes: BoolProperty(
4541 name="Keep strokes",
4542 description="Keeps the sketched strokes or curves after adding the surface",
4543 default=False
4545 SURFSK_automatic_join: BoolProperty(
4546 name="Automatic join",
4547 description="Join automatically vertices of either surfaces "
4548 "generated by crosshatching, or from the borders of closed shapes",
4549 default=True
4551 SURFSK_loops_on_strokes: BoolProperty(
4552 name="Loops on strokes",
4553 description="Make the loops match the paths of the strokes",
4554 default=True
4556 SURFSK_precision: IntProperty(
4557 name="Precision",
4558 description="Precision level of the surface calculation",
4559 default=2,
4560 min=1,
4561 max=100
4563 SURFSK_mesh: PointerProperty(
4564 name="Mesh of BSurface",
4565 type=bpy.types.Object,
4566 description="Mesh of BSurface",
4567 update=update_mesh,
4569 SURFSK_gpencil: PointerProperty(
4570 name="GreasePencil object",
4571 type=bpy.types.Object,
4572 description="GreasePencil object",
4573 update=update_gpencil,
4575 SURFSK_curve: PointerProperty(
4576 name="Curve object",
4577 type=bpy.types.Object,
4578 description="Curve object",
4579 update=update_curve,
4581 SURFSK_mesh_color: FloatVectorProperty(
4582 name="Mesh color",
4583 default=(1.0, 0.0, 0.0, 0.3),
4584 size=4,
4585 subtype="COLOR",
4586 min=0,
4587 max=1,
4588 update=update_color,
4589 description="Mesh color",
4591 SURFSK_Shrinkwrap_offset: FloatProperty(
4592 name="Shrinkwrap offset",
4593 default=0.01,
4594 precision=3,
4595 description="Distance to keep from the target",
4596 update=update_Shrinkwrap_offset,
4598 SURFSK_in_front: BoolProperty(
4599 name="In Front",
4600 description="Make the object draw in front of others",
4601 default=False,
4602 update=update_in_front,
4604 SURFSK_show_wire: BoolProperty(
4605 name="Show wire",
4606 description="Add the object’s wireframe over solid drawing",
4607 default=False,
4608 update=update_show_wire,
4610 SURFSK_shade_smooth: BoolProperty(
4611 name="Shade smooth",
4612 description="Render and display faces smooth, using interpolated Vertex Normals",
4613 default=False,
4614 update=update_shade_smooth,
4617 classes = (
4618 MESH_OT_SURFSK_init,
4619 MESH_OT_SURFSK_add_modifiers,
4620 MESH_OT_SURFSK_add_surface,
4621 MESH_OT_SURFSK_edit_surface,
4622 GPENCIL_OT_SURFSK_add_strokes,
4623 GPENCIL_OT_SURFSK_edit_strokes,
4624 GPENCIL_OT_SURFSK_strokes_to_curves,
4625 GPENCIL_OT_SURFSK_annotation_to_curves,
4626 GPENCIL_OT_SURFSK_add_annotation,
4627 CURVE_OT_SURFSK_edit_curve,
4628 CURVE_OT_SURFSK_reorder_splines,
4629 CURVE_OT_SURFSK_first_points,
4630 BsurfPreferences,
4631 BsurfacesProps
4634 def register():
4635 for cls in classes:
4636 bpy.utils.register_class(cls)
4638 for panel in panels:
4639 bpy.utils.register_class(panel)
4641 bpy.types.Scene.bsurfaces = PointerProperty(type=BsurfacesProps)
4642 update_panel(None, bpy.context)
4644 def unregister():
4645 for panel in panels:
4646 bpy.utils.unregister_class(panel)
4648 for cls in classes:
4649 bpy.utils.unregister_class(cls)
4651 del bpy.types.Scene.bsurfaces
4653 if __name__ == "__main__":
4654 register()