Sun position: Fix T80379 - Custom startup breaks the add-on
[blender-addons.git] / mesh_bsurfaces.py
blob5e3a601c7689bfe55254e2739c844206e7e1a59b
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, 9),
24 "blender": (2, 80, 0),
25 "location": "View3D EditMode > Sidebar > Edit Tab",
26 "description": "Modeling and retopology tool",
27 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
28 "category": "Mesh",
32 import bpy
33 import bmesh
34 from bpy_extras import object_utils
36 import operator
37 from mathutils import Matrix, Vector
38 from mathutils.geometry import (
39 intersect_line_line,
40 intersect_point_line,
42 from math import (
43 degrees,
44 pi,
45 sqrt,
47 from bpy.props import (
48 BoolProperty,
49 FloatProperty,
50 IntProperty,
51 StringProperty,
52 PointerProperty,
53 EnumProperty,
54 FloatVectorProperty,
56 from bpy.types import (
57 Operator,
58 Panel,
59 PropertyGroup,
60 AddonPreferences,
63 # ----------------------------
64 # GLOBAL
65 global_color = [1.0, 0.0, 0.0, 0.3]
66 global_offset = 0.01
67 global_in_front = False
68 global_shade_smooth = False
69 global_show_wire = True
70 global_mesh_object = ""
71 global_gpencil_object = ""
72 global_curve_object = ""
74 # ----------------------------
75 # Panels
76 class VIEW3D_PT_tools_SURFSK_mesh(Panel):
77 bl_space_type = 'VIEW_3D'
78 bl_region_type = 'UI'
79 bl_category = 'Edit'
80 bl_label = "Bsurfaces"
82 def draw(self, context):
83 layout = self.layout
84 scn = context.scene.bsurfaces
86 col = layout.column(align=True)
87 row = layout.row()
88 row.separator()
89 col.operator("mesh.surfsk_init", text="Initialize (Add BSurface mesh)")
90 col.operator("mesh.surfsk_add_modifiers", text="Add Mirror and others modifiers")
92 col.label(text="Mesh of BSurface:")
93 col.prop(scn, "SURFSK_mesh", text="")
94 col.prop(scn, "SURFSK_mesh_color")
95 col.prop(scn, "SURFSK_Shrinkwrap_offset")
96 col.prop(scn, "SURFSK_in_front")
97 col.prop(scn, "SURFSK_shade_smooth")
98 col.prop(scn, "SURFSK_show_wire")
100 col.label(text="Guide strokes:")
101 col.row().prop(scn, "SURFSK_guide", expand=True)
102 if scn.SURFSK_guide == 'GPencil':
103 col.prop(scn, "SURFSK_gpencil", text="")
104 col.separator()
105 if scn.SURFSK_guide == 'Curve':
106 col.prop(scn, "SURFSK_curve", text="")
107 col.separator()
109 col.separator()
110 col.operator("mesh.surfsk_add_surface", text="Add Surface")
111 col.operator("mesh.surfsk_edit_surface", text="Edit Surface")
113 col.separator()
114 if scn.SURFSK_guide == 'GPencil':
115 col.operator("gpencil.surfsk_add_strokes", text="Add Strokes")
116 col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
117 col.separator()
118 col.operator("gpencil.surfsk_strokes_to_curves", text="Strokes to curves")
120 if scn.SURFSK_guide == 'Annotation':
121 col.operator("gpencil.surfsk_add_annotation", text="Add Annotation")
122 col.separator()
123 col.operator("gpencil.surfsk_annotations_to_curves", text="Annotation to curves")
125 if scn.SURFSK_guide == 'Curve':
126 col.operator("curve.surfsk_edit_curve", text="Edit curve")
128 col.separator()
129 col.label(text="Initial settings:")
130 col.prop(scn, "SURFSK_edges_U")
131 col.prop(scn, "SURFSK_edges_V")
132 col.prop(scn, "SURFSK_cyclic_cross")
133 col.prop(scn, "SURFSK_cyclic_follow")
134 col.prop(scn, "SURFSK_loops_on_strokes")
135 col.prop(scn, "SURFSK_automatic_join")
136 col.prop(scn, "SURFSK_keep_strokes")
138 class VIEW3D_PT_tools_SURFSK_curve(Panel):
139 bl_space_type = 'VIEW_3D'
140 bl_region_type = 'UI'
141 bl_context = "curve_edit"
142 bl_category = 'Edit'
143 bl_label = "Bsurfaces"
145 @classmethod
146 def poll(cls, context):
147 return context.active_object
149 def draw(self, context):
150 layout = self.layout
152 col = layout.column(align=True)
153 row = layout.row()
154 row.separator()
155 col.operator("curve.surfsk_first_points", text="Set First Points")
156 col.operator("curve.switch_direction", text="Switch Direction")
157 col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
160 # ----------------------------
161 # Returns the type of strokes used
162 def get_strokes_type(context):
163 strokes_type = "NO_STROKES"
164 strokes_num = 0
166 # Check if they are annotation
167 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
168 try:
169 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
171 strokes_num = len(strokes)
173 if strokes_num > 0:
174 strokes_type = "GP_ANNOTATION"
175 except:
176 strokes_type = "NO_STROKES"
178 # Check if they are grease pencil
179 if context.scene.bsurfaces.SURFSK_guide == 'GPencil':
180 try:
181 global global_gpencil_object
182 gpencil = bpy.data.objects[global_gpencil_object]
183 strokes = gpencil.data.layers.active.active_frame.strokes
185 strokes_num = len(strokes)
187 if strokes_num > 0:
188 strokes_type = "GP_STROKES"
189 except:
190 strokes_type = "NO_STROKES"
192 # Check if they are curves, if there aren't grease pencil strokes
193 if context.scene.bsurfaces.SURFSK_guide == 'Curve':
194 try:
195 global global_curve_object
196 ob = bpy.data.objects[global_curve_object]
197 if ob.type == "CURVE":
198 strokes_type = "EXTERNAL_CURVE"
199 strokes_num = len(ob.data.splines)
201 # Check if there is any non-bezier spline
202 for i in range(len(ob.data.splines)):
203 if ob.data.splines[i].type != "BEZIER":
204 strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
205 break
207 else:
208 strokes_type = "EXTERNAL_NO_CURVE"
209 except:
210 strokes_type = "NO_STROKES"
212 # Check if they are mesh
213 try:
214 global global_mesh_object
215 self.main_object = bpy.data.objects[global_mesh_object]
216 total_vert_sel = len([v for v in self.main_object.data.vertices if v.select])
218 # Check if there is a single stroke without any selection in the object
219 if strokes_num == 1 and total_vert_sel == 0:
220 if strokes_type == "EXTERNAL_CURVE":
221 strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
222 elif strokes_type == "GP_STROKES":
223 strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"
225 if strokes_num == 0 and total_vert_sel > 0:
226 strokes_type = "SELECTION_ALONE"
227 except:
228 pass
230 return strokes_type
232 # ----------------------------
233 # Surface generator operator
234 class MESH_OT_SURFSK_add_surface(Operator):
235 bl_idname = "mesh.surfsk_add_surface"
236 bl_label = "Bsurfaces add surface"
237 bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
238 bl_options = {'REGISTER', 'UNDO'}
240 is_crosshatch: BoolProperty(
241 default=False
243 is_fill_faces: BoolProperty(
244 default=False
246 selection_U_exists: BoolProperty(
247 default=False
249 selection_V_exists: BoolProperty(
250 default=False
252 selection_U2_exists: BoolProperty(
253 default=False
255 selection_V2_exists: BoolProperty(
256 default=False
258 selection_V_is_closed: BoolProperty(
259 default=False
261 selection_U_is_closed: BoolProperty(
262 default=False
264 selection_V2_is_closed: BoolProperty(
265 default=False
267 selection_U2_is_closed: BoolProperty(
268 default=False
271 edges_U: IntProperty(
272 name="Cross",
273 description="Number of face-loops crossing the strokes",
274 default=1,
275 min=1,
276 max=200
278 edges_V: IntProperty(
279 name="Follow",
280 description="Number of face-loops following the strokes",
281 default=1,
282 min=1,
283 max=200
285 cyclic_cross: BoolProperty(
286 name="Cyclic Cross",
287 description="Make cyclic the face-loops crossing the strokes",
288 default=False
290 cyclic_follow: BoolProperty(
291 name="Cyclic Follow",
292 description="Make cyclic the face-loops following the strokes",
293 default=False
295 loops_on_strokes: BoolProperty(
296 name="Loops on strokes",
297 description="Make the loops match the paths of the strokes",
298 default=False
300 automatic_join: BoolProperty(
301 name="Automatic join",
302 description="Join automatically vertices of either surfaces generated "
303 "by crosshatching, or from the borders of closed shapes",
304 default=False
306 join_stretch_factor: FloatProperty(
307 name="Stretch",
308 description="Amount of stretching or shrinking allowed for "
309 "edges when joining vertices automatically",
310 default=1,
311 min=0,
312 max=3,
313 subtype='FACTOR'
315 keep_strokes: BoolProperty(
316 name="Keep strokes",
317 description="Keeps the sketched strokes or curves after adding the surface",
318 default=False
320 strokes_type: StringProperty()
321 initial_global_undo_state: BoolProperty()
324 def draw(self, context):
325 layout = self.layout
326 col = layout.column(align=True)
327 row = layout.row()
329 if not self.is_fill_faces:
330 row.separator()
331 if not self.is_crosshatch:
332 if not self.selection_U_exists:
333 col.prop(self, "edges_U")
334 row.separator()
336 if not self.selection_V_exists:
337 col.prop(self, "edges_V")
338 row.separator()
340 row.separator()
342 if not self.selection_U_exists:
343 if not (
344 (self.selection_V_exists and not self.selection_V_is_closed) or
345 (self.selection_V2_exists and not self.selection_V2_is_closed)
347 col.prop(self, "cyclic_cross")
349 if not self.selection_V_exists:
350 if not (
351 (self.selection_U_exists and not self.selection_U_is_closed) or
352 (self.selection_U2_exists and not self.selection_U2_is_closed)
354 col.prop(self, "cyclic_follow")
356 col.prop(self, "loops_on_strokes")
358 col.prop(self, "automatic_join")
360 if self.automatic_join:
361 row.separator()
362 col.separator()
363 row.separator()
364 col.prop(self, "join_stretch_factor")
366 col.prop(self, "keep_strokes")
368 # Get an ordered list of a chain of vertices
369 def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx,
370 first_vert_idx, middle_vertex_idx, closing_vert_idx):
371 # Order selected vertices.
372 verts_ordered = []
373 if closing_vert_idx is not None:
374 verts_ordered.append(ob.data.vertices[closing_vert_idx])
376 verts_ordered.append(ob.data.vertices[first_vert_idx])
377 prev_v = first_vert_idx
378 prev_ed = None
379 finish_while = False
380 while True:
381 edges_non_matched = 0
382 for i in all_selected_edges_idx:
383 if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and \
384 ob.data.edges[i].vertices[1] in all_selected_verts_idx:
386 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
387 prev_v = ob.data.edges[i].vertices[1]
388 prev_ed = ob.data.edges[i]
389 elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and \
390 ob.data.edges[i].vertices[0] in all_selected_verts_idx:
392 verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
393 prev_v = ob.data.edges[i].vertices[0]
394 prev_ed = ob.data.edges[i]
395 else:
396 edges_non_matched += 1
398 if edges_non_matched == len(all_selected_edges_idx):
399 finish_while = True
401 if finish_while:
402 break
404 if closing_vert_idx is not None:
405 verts_ordered.append(ob.data.vertices[closing_vert_idx])
407 if middle_vertex_idx is not None:
408 verts_ordered.append(ob.data.vertices[middle_vertex_idx])
409 verts_ordered.reverse()
411 return tuple(verts_ordered)
413 # Calculates length of a chain of points.
414 def get_chain_length(self, object, verts_ordered):
415 matrix = object.matrix_world
417 edges_lengths = []
418 edges_lengths_sum = 0
419 for i in range(0, len(verts_ordered)):
420 if i == 0:
421 prev_v_co = matrix @ verts_ordered[i].co
422 else:
423 v_co = matrix @ verts_ordered[i].co
425 v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
426 edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
428 edges_lengths.append(edge_length)
429 edges_lengths_sum += edge_length
431 prev_v_co = v_co
433 return edges_lengths, edges_lengths_sum
435 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
436 def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
437 edges_proportions = []
438 if use_boundaries:
439 verts_count = 1
440 for l in edges_lengths:
441 edges_proportions.append(l / edges_lengths_sum)
442 verts_count += 1
443 else:
444 verts_count = 1
445 for _n in range(0, fixed_edges_num):
446 edges_proportions.append(1 / fixed_edges_num)
447 verts_count += 1
449 return edges_proportions
451 # Calculates the angle between two pairs of points in space
452 def orientation_difference(self, points_A_co, points_B_co):
453 # each parameter should be a list with two elements,
454 # and each element should be a x,y,z coordinate
455 vec_A = points_A_co[0] - points_A_co[1]
456 vec_B = points_B_co[0] - points_B_co[1]
458 angle = vec_A.angle(vec_B)
460 if angle > 0.5 * pi:
461 angle = abs(angle - pi)
463 return angle
465 # Calculate the which vert of verts_idx list is the nearest one
466 # to the point_co coordinates, and the distance
467 def shortest_distance(self, object, point_co, verts_idx):
468 matrix = object.matrix_world
470 for i in range(0, len(verts_idx)):
471 dist = (point_co - matrix @ object.data.vertices[verts_idx[i]].co).length
472 if i == 0:
473 prev_dist = dist
474 nearest_vert_idx = verts_idx[i]
475 shortest_dist = dist
477 if dist < prev_dist:
478 prev_dist = dist
479 nearest_vert_idx = verts_idx[i]
480 shortest_dist = dist
482 return nearest_vert_idx, shortest_dist
484 # Returns the index of the opposite vert tip in a chain, given a vert tip index
485 # as parameter, and a multidimentional list with all pairs of tips
486 def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
487 opposite_vert_tip_idx = None
488 for i in range(0, len(all_chains_tips_idx)):
489 if vert_tip_idx == all_chains_tips_idx[i][0]:
490 opposite_vert_tip_idx = all_chains_tips_idx[i][1]
491 if vert_tip_idx == all_chains_tips_idx[i][1]:
492 opposite_vert_tip_idx = all_chains_tips_idx[i][0]
494 return opposite_vert_tip_idx
496 # Simplifies a spline and returns the new points coordinates
497 def simplify_spline(self, spline_coords, segments_num):
498 simplified_spline = []
499 points_between_segments = round(len(spline_coords) / segments_num)
501 simplified_spline.append(spline_coords[0])
502 for i in range(1, segments_num):
503 simplified_spline.append(spline_coords[i * points_between_segments])
505 simplified_spline.append(spline_coords[len(spline_coords) - 1])
507 return simplified_spline
509 # Returns a list with the coords of the points distributed over the splines
510 # passed to this method according to the proportions parameter
511 def distribute_pts(self, surface_splines, proportions):
513 # Calculate the length of each final surface spline
514 surface_splines_lengths = []
515 surface_splines_parsed = []
517 for sp_idx in range(0, len(surface_splines)):
518 # Calculate spline length
519 surface_splines_lengths.append(0)
521 for i in range(0, len(surface_splines[sp_idx].bezier_points)):
522 if i == 0:
523 prev_p = surface_splines[sp_idx].bezier_points[i]
524 else:
525 p = surface_splines[sp_idx].bezier_points[i]
526 edge_length = (prev_p.co - p.co).length
527 surface_splines_lengths[sp_idx] += edge_length
529 prev_p = p
531 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
532 for sp_idx in range(0, len(surface_splines)):
533 surface_splines_parsed.append([])
534 surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
536 prev_p_co = surface_splines[sp_idx].bezier_points[0].co
537 p_idx = 0
539 for prop_idx in range(len(proportions) - 1):
540 target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
541 partial_segment_length = 0
542 finish_while = False
544 while True:
545 # if not it'll pass the p_idx as an index below and crash
546 if p_idx < len(surface_splines[sp_idx].bezier_points):
547 p_co = surface_splines[sp_idx].bezier_points[p_idx].co
548 new_dist = (prev_p_co - p_co).length
550 # The new distance that could have the partial segment if
551 # it is still shorter than the target length
552 potential_segment_length = partial_segment_length + new_dist
554 # If the potential is still shorter, keep adding
555 if potential_segment_length < target_length:
556 partial_segment_length = potential_segment_length
558 p_idx += 1
559 prev_p_co = p_co
561 # If the potential is longer than the target, calculate the target
562 # (a point between the last two points), and assign
563 elif potential_segment_length > target_length:
564 remaining_dist = target_length - partial_segment_length
565 vec = p_co - prev_p_co
566 vec.normalize()
567 intermediate_co = prev_p_co + (vec * remaining_dist)
569 surface_splines_parsed[sp_idx].append(intermediate_co)
571 partial_segment_length += remaining_dist
572 prev_p_co = intermediate_co
574 finish_while = True
576 # If the potential is equal to the target, assign
577 elif potential_segment_length == target_length:
578 surface_splines_parsed[sp_idx].append(p_co)
579 prev_p_co = p_co
581 finish_while = True
583 if finish_while:
584 break
586 # last point of the spline
587 surface_splines_parsed[sp_idx].append(
588 surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
591 return surface_splines_parsed
593 # Counts the number of faces that belong to each edge
594 def edge_face_count(self, ob):
595 ed_keys_count_dict = {}
597 for face in ob.data.polygons:
598 for ed_keys in face.edge_keys:
599 if ed_keys not in ed_keys_count_dict:
600 ed_keys_count_dict[ed_keys] = 1
601 else:
602 ed_keys_count_dict[ed_keys] += 1
604 edge_face_count = []
605 for i in range(len(ob.data.edges)):
606 edge_face_count.append(0)
608 for i in range(len(ob.data.edges)):
609 ed = ob.data.edges[i]
611 v1 = ed.vertices[0]
612 v2 = ed.vertices[1]
614 if (v1, v2) in ed_keys_count_dict:
615 edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
616 elif (v2, v1) in ed_keys_count_dict:
617 edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
619 return edge_face_count
621 # Fills with faces all the selected vertices which form empty triangles or quads
622 def fill_with_faces(self, object):
623 all_selected_verts_count = self.main_object_selected_verts_count
625 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
627 # Calculate average length of selected edges
628 all_selected_verts = []
629 original_sel_edges_count = 0
630 for ed in object.data.edges:
631 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
632 coords = []
633 coords.append(object.data.vertices[ed.vertices[0]].co)
634 coords.append(object.data.vertices[ed.vertices[1]].co)
636 original_sel_edges_count += 1
638 if not ed.vertices[0] in all_selected_verts:
639 all_selected_verts.append(ed.vertices[0])
641 if not ed.vertices[1] in all_selected_verts:
642 all_selected_verts.append(ed.vertices[1])
644 tuple(all_selected_verts)
646 # Check if there is any edge selected. If not, interrupt the script
647 if original_sel_edges_count == 0 and all_selected_verts_count > 0:
648 return 0
650 # Get all edges connected to selected verts
651 all_edges_around_sel_verts = []
652 edges_connected_to_sel_verts = {}
653 verts_connected_to_every_vert = {}
654 for ed_idx in range(len(object.data.edges)):
655 ed = object.data.edges[ed_idx]
656 include_edge = False
658 if ed.vertices[0] in all_selected_verts:
659 if not ed.vertices[0] in edges_connected_to_sel_verts:
660 edges_connected_to_sel_verts[ed.vertices[0]] = []
662 edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
663 include_edge = True
665 if ed.vertices[1] in all_selected_verts:
666 if not ed.vertices[1] in edges_connected_to_sel_verts:
667 edges_connected_to_sel_verts[ed.vertices[1]] = []
669 edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
670 include_edge = True
672 if include_edge is True:
673 all_edges_around_sel_verts.append(ed_idx)
675 # Get all connected verts to each vert
676 if not ed.vertices[0] in verts_connected_to_every_vert:
677 verts_connected_to_every_vert[ed.vertices[0]] = []
679 if not ed.vertices[1] in verts_connected_to_every_vert:
680 verts_connected_to_every_vert[ed.vertices[1]] = []
682 verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
683 verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
685 # Get all verts connected to faces
686 all_verts_part_of_faces = []
687 all_edges_faces_count = []
688 all_edges_faces_count += self.edge_face_count(object)
690 # Get only the selected edges that have faces attached.
691 count_faces_of_edges_around_sel_verts = {}
692 selected_verts_with_faces = []
693 for ed_idx in all_edges_around_sel_verts:
694 count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
696 if all_edges_faces_count[ed_idx] > 0:
697 ed = object.data.edges[ed_idx]
699 if not ed.vertices[0] in selected_verts_with_faces:
700 selected_verts_with_faces.append(ed.vertices[0])
702 if not ed.vertices[1] in selected_verts_with_faces:
703 selected_verts_with_faces.append(ed.vertices[1])
705 all_verts_part_of_faces.append(ed.vertices[0])
706 all_verts_part_of_faces.append(ed.vertices[1])
708 tuple(selected_verts_with_faces)
710 # Discard unneeded verts from calculations
711 participating_verts = []
712 movable_verts = []
713 for v_idx in all_selected_verts:
714 vert_has_edges_with_one_face = False
716 # Check if the actual vert has at least one edge connected to only one face
717 for ed_idx in edges_connected_to_sel_verts[v_idx]:
718 if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
719 vert_has_edges_with_one_face = True
721 # If the vert has two or less edges connected and the vert is not part of any face.
722 # Or the vert is part of any face and at least one of
723 # the connected edges has only one face attached to it.
724 if (len(edges_connected_to_sel_verts[v_idx]) == 2 and
725 v_idx not in all_verts_part_of_faces) or \
726 len(edges_connected_to_sel_verts[v_idx]) == 1 or \
727 (v_idx in all_verts_part_of_faces and
728 vert_has_edges_with_one_face):
730 participating_verts.append(v_idx)
732 if v_idx not in all_verts_part_of_faces:
733 movable_verts.append(v_idx)
735 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
736 for mv_idx in movable_verts:
737 freeze_vert = False
738 mv_connected_verts = verts_connected_to_every_vert[mv_idx]
740 for actual_v_idx in all_selected_verts:
741 count_shared_neighbors = 0
742 checked_verts = []
744 for mv_conn_v_idx in mv_connected_verts:
745 if mv_idx != actual_v_idx:
746 if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and \
747 mv_conn_v_idx not in checked_verts:
748 count_shared_neighbors += 1
749 checked_verts.append(mv_conn_v_idx)
751 if actual_v_idx in mv_connected_verts:
752 freeze_vert = True
753 break
755 if count_shared_neighbors == 2:
756 freeze_vert = True
757 break
759 if freeze_vert:
760 break
762 if freeze_vert:
763 movable_verts.remove(mv_idx)
765 # Calculate merge distance for participating verts
766 shortest_edge_length = None
767 for ed in object.data.edges:
768 if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
769 v1 = object.data.vertices[ed.vertices[0]]
770 v2 = object.data.vertices[ed.vertices[1]]
772 length = (v1.co - v2.co).length
774 if shortest_edge_length is None:
775 shortest_edge_length = length
776 else:
777 if length < shortest_edge_length:
778 shortest_edge_length = length
780 if shortest_edge_length is not None:
781 edges_merge_distance = shortest_edge_length * 0.5
782 else:
783 edges_merge_distance = 0
785 # Get together the verts near enough. They will be merged later
786 remaining_verts = []
787 remaining_verts += participating_verts
788 for v1_idx in participating_verts:
789 if v1_idx in remaining_verts and v1_idx in movable_verts:
790 verts_to_merge = []
791 coords_verts_to_merge = {}
793 verts_to_merge.append(v1_idx)
795 v1_co = object.data.vertices[v1_idx].co
796 coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
798 for v2_idx in remaining_verts:
799 if v1_idx != v2_idx:
800 v2_co = object.data.vertices[v2_idx].co
802 dist = (v1_co - v2_co).length
804 if dist <= edges_merge_distance: # Add the verts which are near enough
805 verts_to_merge.append(v2_idx)
807 coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
809 for vm_idx in verts_to_merge:
810 remaining_verts.remove(vm_idx)
812 if len(verts_to_merge) > 1:
813 # Calculate middle point of the verts to merge.
814 sum_x_co = 0
815 sum_y_co = 0
816 sum_z_co = 0
817 movable_verts_to_merge_count = 0
818 for i in range(len(verts_to_merge)):
819 if verts_to_merge[i] in movable_verts:
820 v_co = object.data.vertices[verts_to_merge[i]].co
822 sum_x_co += v_co[0]
823 sum_y_co += v_co[1]
824 sum_z_co += v_co[2]
826 movable_verts_to_merge_count += 1
828 middle_point_co = [
829 sum_x_co / movable_verts_to_merge_count,
830 sum_y_co / movable_verts_to_merge_count,
831 sum_z_co / movable_verts_to_merge_count
834 # Check if any vert to be merged is not movable
835 shortest_dist = None
836 are_verts_not_movable = False
837 verts_not_movable = []
838 for v_merge_idx in verts_to_merge:
839 if v_merge_idx in participating_verts and v_merge_idx not in movable_verts:
840 are_verts_not_movable = True
841 verts_not_movable.append(v_merge_idx)
843 if are_verts_not_movable:
844 # Get the vert connected to faces, that is nearest to
845 # the middle point of the movable verts
846 shortest_dist = None
847 for vcf_idx in verts_not_movable:
848 dist = abs((object.data.vertices[vcf_idx].co -
849 Vector(middle_point_co)).length)
851 if shortest_dist is None:
852 shortest_dist = dist
853 nearest_vert_idx = vcf_idx
854 else:
855 if dist < shortest_dist:
856 shortest_dist = dist
857 nearest_vert_idx = vcf_idx
859 coords = object.data.vertices[nearest_vert_idx].co
860 target_point_co = [coords[0], coords[1], coords[2]]
861 else:
862 target_point_co = middle_point_co
864 # Move verts to merge to the middle position
865 for v_merge_idx in verts_to_merge:
866 if v_merge_idx in movable_verts: # Only move the verts that are not part of faces
867 object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
868 object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
869 object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
871 # Perform "Remove Doubles" to weld all the disconnected verts
872 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
873 bpy.ops.mesh.remove_doubles(threshold=0.0001)
875 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
877 # Get all the definitive selected edges, after weldding
878 selected_edges = []
879 edges_per_vert = {} # Number of faces of each selected edge
880 for ed in object.data.edges:
881 if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
882 selected_edges.append(ed.index)
884 # Save all the edges that belong to each vertex.
885 if not ed.vertices[0] in edges_per_vert:
886 edges_per_vert[ed.vertices[0]] = []
888 if not ed.vertices[1] in edges_per_vert:
889 edges_per_vert[ed.vertices[1]] = []
891 edges_per_vert[ed.vertices[0]].append(ed.index)
892 edges_per_vert[ed.vertices[1]].append(ed.index)
894 # Check if all the edges connected to each vert have two faces attached to them.
895 # To discard them later and make calculations faster
896 a = []
897 a += self.edge_face_count(object)
898 tuple(a)
899 verts_surrounded_by_faces = {}
900 for v_idx in edges_per_vert:
901 edges_with_two_faces_count = 0
903 for ed_idx in edges_per_vert[v_idx]:
904 if a[ed_idx] == 2:
905 edges_with_two_faces_count += 1
907 if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
908 verts_surrounded_by_faces[v_idx] = True
909 else:
910 verts_surrounded_by_faces[v_idx] = False
912 # Get all the selected vertices
913 selected_verts_idx = []
914 for v in object.data.vertices:
915 if v.select:
916 selected_verts_idx.append(v.index)
918 # Get all the faces of the object
919 all_object_faces_verts_idx = []
920 for face in object.data.polygons:
921 face_verts = []
922 face_verts.append(face.vertices[0])
923 face_verts.append(face.vertices[1])
924 face_verts.append(face.vertices[2])
926 if len(face.vertices) == 4:
927 face_verts.append(face.vertices[3])
929 all_object_faces_verts_idx.append(face_verts)
931 # Deselect all vertices
932 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
933 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
934 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
936 # Make a dictionary with the verts related to each vert
937 related_key_verts = {}
938 for ed_idx in selected_edges:
939 ed = object.data.edges[ed_idx]
941 if not verts_surrounded_by_faces[ed.vertices[0]]:
942 if not ed.vertices[0] in related_key_verts:
943 related_key_verts[ed.vertices[0]] = []
945 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
946 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
948 if not verts_surrounded_by_faces[ed.vertices[1]]:
949 if not ed.vertices[1] in related_key_verts:
950 related_key_verts[ed.vertices[1]] = []
952 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
953 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
955 # Get groups of verts forming each face
956 faces_verts_idx = []
957 for v1 in related_key_verts: # verts-1 ....
958 for v2 in related_key_verts: # verts-2
959 if v1 != v2:
960 related_verts_in_common = []
961 v2_in_rel_v1 = False
962 v1_in_rel_v2 = False
963 for rel_v1 in related_key_verts[v1]:
964 # Check if related verts of verts-1 are related verts of verts-2
965 if rel_v1 in related_key_verts[v2]:
966 related_verts_in_common.append(rel_v1)
968 if v2 in related_key_verts[v1]:
969 v2_in_rel_v1 = True
971 if v1 in related_key_verts[v2]:
972 v1_in_rel_v2 = True
974 repeated_face = False
975 # If two verts have two related verts in common, they form a quad
976 if len(related_verts_in_common) == 2:
977 # Check if the face is already saved
978 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
980 for f_verts in all_faces_to_check_idx:
981 repeated_verts = 0
983 if len(f_verts) == 4:
984 if v1 in f_verts:
985 repeated_verts += 1
986 if v2 in f_verts:
987 repeated_verts += 1
988 if related_verts_in_common[0] in f_verts:
989 repeated_verts += 1
990 if related_verts_in_common[1] in f_verts:
991 repeated_verts += 1
993 if repeated_verts == len(f_verts):
994 repeated_face = True
995 break
997 if not repeated_face:
998 faces_verts_idx.append(
999 [v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
1002 # If Two verts have one related vert in common and
1003 # they are related to each other, they form a triangle
1004 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1005 # Check if the face is already saved.
1006 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1008 for f_verts in all_faces_to_check_idx:
1009 repeated_verts = 0
1011 if len(f_verts) == 3:
1012 if v1 in f_verts:
1013 repeated_verts += 1
1014 if v2 in f_verts:
1015 repeated_verts += 1
1016 if related_verts_in_common[0] in f_verts:
1017 repeated_verts += 1
1019 if repeated_verts == len(f_verts):
1020 repeated_face = True
1021 break
1023 if not repeated_face:
1024 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1026 # Keep only the faces that don't overlap by ignoring quads
1027 # that overlap with two adjacent triangles
1028 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1029 all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
1030 for i in range(len(faces_verts_idx)):
1031 for t in range(len(all_faces_to_check_idx)):
1032 if i != t:
1033 verts_in_common = 0
1035 if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
1036 for v_idx in all_faces_to_check_idx[t]:
1037 if v_idx in faces_verts_idx[i]:
1038 verts_in_common += 1
1039 # If it doesn't have all it's vertices repeated in the other face
1040 if verts_in_common == 3:
1041 if i not in faces_to_not_include_idx:
1042 faces_to_not_include_idx.append(i)
1044 # Build faces discarding the ones in faces_to_not_include
1045 me = object.data
1046 bm = bmesh.new()
1047 bm.from_mesh(me)
1049 num_faces_created = 0
1050 for i in range(len(faces_verts_idx)):
1051 if i not in faces_to_not_include_idx:
1052 bm.faces.new([bm.verts[v] for v in faces_verts_idx[i]])
1054 num_faces_created += 1
1056 bm.to_mesh(me)
1057 bm.free()
1059 for v_idx in selected_verts_idx:
1060 self.main_object.data.vertices[v_idx].select = True
1062 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
1063 bpy.ops.mesh.normals_make_consistent(inside=False)
1064 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
1066 self.update()
1068 return num_faces_created
1070 # Crosshatch skinning
1071 def crosshatch_surface_invoke(self, ob_original_splines):
1072 self.is_crosshatch = False
1073 self.crosshatch_merge_distance = 0
1075 objects_to_delete = [] # duplicated strokes to be deleted.
1077 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1078 # (without this the surface verts merging with the main object doesn't work well)
1079 self.modifiers_prev_viewport_state = []
1080 if len(self.main_object.modifiers) > 0:
1081 for m_idx in range(len(self.main_object.modifiers)):
1082 self.modifiers_prev_viewport_state.append(
1083 self.main_object.modifiers[m_idx].show_viewport
1085 self.main_object.modifiers[m_idx].show_viewport = False
1087 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1088 ob_original_splines.select_set(True)
1089 bpy.context.view_layer.objects.active = ob_original_splines
1091 if len(ob_original_splines.data.splines) >= 2:
1092 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1093 ob_splines = bpy.context.object
1094 ob_splines.name = "SURFSKIO_NE_STR"
1096 # Get estimative merge distance (sum up the distances from the first point to
1097 # all other points, then average them and then divide them)
1098 first_point_dist_sum = 0
1099 first_dist = 0
1100 second_dist = 0
1101 coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
1102 for i in range(len(ob_splines.data.splines)):
1103 sp = ob_splines.data.splines[i]
1105 if coords_first_pt != sp.bezier_points[0].co:
1106 first_dist = (coords_first_pt - sp.bezier_points[0].co).length
1108 if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
1109 second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
1111 first_point_dist_sum += first_dist + second_dist
1113 if i == 0:
1114 if first_dist != 0:
1115 shortest_dist = first_dist
1116 elif second_dist != 0:
1117 shortest_dist = second_dist
1119 if shortest_dist > first_dist and first_dist != 0:
1120 shortest_dist = first_dist
1122 if shortest_dist > second_dist and second_dist != 0:
1123 shortest_dist = second_dist
1125 self.crosshatch_merge_distance = shortest_dist / 20
1127 # Recalculation of merge distance
1129 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1131 ob_calc_merge_dist = bpy.context.object
1132 ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
1134 objects_to_delete.append(ob_calc_merge_dist)
1136 # Smooth out strokes a little to improve crosshatch detection
1137 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1138 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
1140 for i in range(4):
1141 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1143 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1144 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1146 # Convert curves into mesh
1147 ob_calc_merge_dist.data.resolution_u = 12
1148 bpy.ops.object.convert(target='MESH', keep_original=False)
1150 # Find "intersection-nodes"
1151 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1152 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1153 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1154 threshold=self.crosshatch_merge_distance)
1155 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1156 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1158 # Remove verts with less than three edges
1159 verts_edges_count = {}
1160 for ed in ob_calc_merge_dist.data.edges:
1161 v = ed.vertices
1163 if v[0] not in verts_edges_count:
1164 verts_edges_count[v[0]] = 0
1166 if v[1] not in verts_edges_count:
1167 verts_edges_count[v[1]] = 0
1169 verts_edges_count[v[0]] += 1
1170 verts_edges_count[v[1]] += 1
1172 nodes_verts_coords = []
1173 for v_idx in verts_edges_count:
1174 v = ob_calc_merge_dist.data.vertices[v_idx]
1176 if verts_edges_count[v_idx] < 3:
1177 v.select = True
1179 # Remove them
1180 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1181 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
1182 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1184 # Remove doubles to discard very near verts from calculations of distance
1185 bpy.ops.mesh.remove_doubles(
1186 'INVOKE_REGION_WIN',
1187 threshold=self.crosshatch_merge_distance * 4.0
1189 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1191 # Get all coords of the resulting nodes
1192 nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for
1193 v in ob_calc_merge_dist.data.vertices]
1195 # Check if the strokes are a crosshatch
1196 if len(nodes_verts_coords) >= 3:
1197 self.is_crosshatch = True
1199 shortest_dist = None
1200 for co_1 in nodes_verts_coords:
1201 for co_2 in nodes_verts_coords:
1202 if co_1 != co_2:
1203 dist = (Vector(co_1) - Vector(co_2)).length
1205 if shortest_dist is not None:
1206 if dist < shortest_dist:
1207 shortest_dist = dist
1208 else:
1209 shortest_dist = dist
1211 self.crosshatch_merge_distance = shortest_dist / 3
1213 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1214 ob_splines.select_set(True)
1215 bpy.context.view_layer.objects.active = ob_splines
1217 # Deselect all points
1218 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1219 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1220 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1222 # Smooth splines in a localized way, to eliminate "saw-teeth"
1223 # like shapes when there are many points
1224 for sp in ob_splines.data.splines:
1225 angle_sum = 0
1227 angle_limit = 2 # Degrees
1228 for t in range(len(sp.bezier_points)):
1229 # Because on each iteration it checks the "next two points"
1230 # of the actual. This way it doesn't go out of range
1231 if t <= len(sp.bezier_points) - 3:
1232 p1 = sp.bezier_points[t]
1233 p2 = sp.bezier_points[t + 1]
1234 p3 = sp.bezier_points[t + 2]
1236 vec_1 = p1.co - p2.co
1237 vec_2 = p2.co - p3.co
1239 if p2.co != p1.co and p2.co != p3.co:
1240 angle = vec_1.angle(vec_2)
1241 angle_sum += degrees(angle)
1243 if angle_sum >= angle_limit: # If sum of angles is grater than the limit
1244 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1245 p1.select_control_point = True
1246 p1.select_left_handle = True
1247 p1.select_right_handle = True
1249 p2.select_control_point = True
1250 p2.select_left_handle = True
1251 p2.select_right_handle = True
1253 if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
1254 p3.select_control_point = True
1255 p3.select_left_handle = True
1256 p3.select_right_handle = True
1258 angle_sum = 0
1260 sp.bezier_points[0].select_control_point = False
1261 sp.bezier_points[0].select_left_handle = False
1262 sp.bezier_points[0].select_right_handle = False
1264 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
1265 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
1266 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
1268 # Smooth out strokes a little to improve crosshatch detection
1269 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1271 for i in range(15):
1272 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1274 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1275 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1277 # Simplify the splines
1278 for sp in ob_splines.data.splines:
1279 angle_sum = 0
1281 sp.bezier_points[0].select_control_point = True
1282 sp.bezier_points[0].select_left_handle = True
1283 sp.bezier_points[0].select_right_handle = True
1285 sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
1286 sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
1287 sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
1289 angle_limit = 15 # Degrees
1290 for t in range(len(sp.bezier_points)):
1291 # Because on each iteration it checks the "next two points"
1292 # of the actual. This way it doesn't go out of range
1293 if t <= len(sp.bezier_points) - 3:
1294 p1 = sp.bezier_points[t]
1295 p2 = sp.bezier_points[t + 1]
1296 p3 = sp.bezier_points[t + 2]
1298 vec_1 = p1.co - p2.co
1299 vec_2 = p2.co - p3.co
1301 if p2.co != p1.co and p2.co != p3.co:
1302 angle = vec_1.angle(vec_2)
1303 angle_sum += degrees(angle)
1304 # If sum of angles is grater than the limit
1305 if angle_sum >= angle_limit:
1306 p1.select_control_point = True
1307 p1.select_left_handle = True
1308 p1.select_right_handle = True
1310 p2.select_control_point = True
1311 p2.select_left_handle = True
1312 p2.select_right_handle = True
1314 p3.select_control_point = True
1315 p3.select_left_handle = True
1316 p3.select_right_handle = True
1318 angle_sum = 0
1320 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1321 bpy.ops.curve.select_all(action='INVERT')
1323 bpy.ops.curve.delete(type='VERT')
1324 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1326 objects_to_delete.append(ob_splines)
1328 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1329 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1330 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1332 # Check if the strokes are a crosshatch
1333 if self.is_crosshatch:
1334 all_points_coords = []
1335 for i in range(len(ob_splines.data.splines)):
1336 all_points_coords.append([])
1338 all_points_coords[i] = [Vector((x, y, z)) for
1339 x, y, z in [bp.co for
1340 bp in ob_splines.data.splines[i].bezier_points]]
1342 all_intersections = []
1343 checked_splines = []
1344 for i in range(len(all_points_coords)):
1346 for t in range(len(all_points_coords[i]) - 1):
1347 bp1_co = all_points_coords[i][t]
1348 bp2_co = all_points_coords[i][t + 1]
1350 for i2 in range(len(all_points_coords)):
1351 if i != i2 and i2 not in checked_splines:
1352 for t2 in range(len(all_points_coords[i2]) - 1):
1353 bp3_co = all_points_coords[i2][t2]
1354 bp4_co = all_points_coords[i2][t2 + 1]
1356 intersec_coords = intersect_line_line(
1357 bp1_co, bp2_co, bp3_co, bp4_co
1359 if intersec_coords is not None:
1360 dist = (intersec_coords[0] - intersec_coords[1]).length
1362 if dist <= self.crosshatch_merge_distance * 1.5:
1363 _temp_co, percent1 = intersect_point_line(
1364 intersec_coords[0], bp1_co, bp2_co
1366 if (percent1 >= -0.02 and percent1 <= 1.02):
1367 _temp_co, percent2 = intersect_point_line(
1368 intersec_coords[1], bp3_co, bp4_co
1370 if (percent2 >= -0.02 and percent2 <= 1.02):
1371 # Format: spline index, first point index from
1372 # corresponding segment, percentage from first point of
1373 # actual segment, coords of intersection point
1374 all_intersections.append(
1375 (i, t, percent1,
1376 ob_splines.matrix_world @ intersec_coords[0])
1378 all_intersections.append(
1379 (i2, t2, percent2,
1380 ob_splines.matrix_world @ intersec_coords[1])
1383 checked_splines.append(i)
1384 # Sort list by spline, then by corresponding first point index of segment,
1385 # and then by percentage from first point of segment: elements 0 and 1 respectively
1386 all_intersections.sort(key=operator.itemgetter(0, 1, 2))
1388 self.crosshatch_strokes_coords = {}
1389 for i in range(len(all_intersections)):
1390 if not all_intersections[i][0] in self.crosshatch_strokes_coords:
1391 self.crosshatch_strokes_coords[all_intersections[i][0]] = []
1393 self.crosshatch_strokes_coords[all_intersections[i][0]].append(
1394 all_intersections[i][3]
1395 ) # Save intersection coords
1396 else:
1397 self.is_crosshatch = False
1399 # Delete all duplicates
1400 bpy.ops.object.delete({"selected_objects": objects_to_delete})
1402 # If the main object has modifiers, turn their "viewport view status" to
1403 # what it was before the forced deactivation above
1404 if len(self.main_object.modifiers) > 0:
1405 for m_idx in range(len(self.main_object.modifiers)):
1406 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1408 self.update()
1410 return
1412 # Part of the Crosshatch process that is repeated when the operator is tweaked
1413 def crosshatch_surface_execute(self, context):
1414 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1415 # (without this the surface verts merging with the main object doesn't work well)
1416 self.modifiers_prev_viewport_state = []
1417 if len(self.main_object.modifiers) > 0:
1418 for m_idx in range(len(self.main_object.modifiers)):
1419 self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
1421 self.main_object.modifiers[m_idx].show_viewport = False
1423 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1425 me_name = "SURFSKIO_STK_TMP"
1426 me = bpy.data.meshes.new(me_name)
1428 all_verts_coords = []
1429 all_edges = []
1430 for st_idx in self.crosshatch_strokes_coords:
1431 for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
1432 coords = self.crosshatch_strokes_coords[st_idx][co_idx]
1434 all_verts_coords.append(coords)
1436 if co_idx > 0:
1437 all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
1439 me.from_pydata(all_verts_coords, all_edges, [])
1440 ob = object_utils.object_data_add(context, me)
1441 ob.location = (0.0, 0.0, 0.0)
1442 ob.rotation_euler = (0.0, 0.0, 0.0)
1443 ob.scale = (1.0, 1.0, 1.0)
1445 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1446 ob.select_set(True)
1447 bpy.context.view_layer.objects.active = ob
1449 # Get together each vert and its nearest, to the middle position
1450 verts = ob.data.vertices
1451 checked_verts = []
1452 for i in range(len(verts)):
1453 shortest_dist = None
1455 if i not in checked_verts:
1456 for t in range(len(verts)):
1457 if i != t and t not in checked_verts:
1458 dist = (verts[i].co - verts[t].co).length
1460 if shortest_dist is not None:
1461 if dist < shortest_dist:
1462 shortest_dist = dist
1463 nearest_vert = t
1464 else:
1465 shortest_dist = dist
1466 nearest_vert = t
1468 middle_location = (verts[i].co + verts[nearest_vert].co) / 2
1470 verts[i].co = middle_location
1471 verts[nearest_vert].co = middle_location
1473 checked_verts.append(i)
1474 checked_verts.append(nearest_vert)
1476 # Calculate average length between all the generated edges
1477 ob = bpy.context.object
1478 lengths_sum = 0
1479 for ed in ob.data.edges:
1480 v1 = ob.data.vertices[ed.vertices[0]]
1481 v2 = ob.data.vertices[ed.vertices[1]]
1483 lengths_sum += (v1.co - v2.co).length
1485 edges_count = len(ob.data.edges)
1486 # possible division by zero here
1487 average_edge_length = lengths_sum / edges_count if edges_count != 0 else 0.0001
1489 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1490 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
1491 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
1492 threshold=average_edge_length / 15.0)
1493 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1495 final_points_ob = bpy.context.view_layer.objects.active
1497 # Make a dictionary with the verts related to each vert
1498 related_key_verts = {}
1499 for ed in final_points_ob.data.edges:
1500 if not ed.vertices[0] in related_key_verts:
1501 related_key_verts[ed.vertices[0]] = []
1503 if not ed.vertices[1] in related_key_verts:
1504 related_key_verts[ed.vertices[1]] = []
1506 if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
1507 related_key_verts[ed.vertices[0]].append(ed.vertices[1])
1509 if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
1510 related_key_verts[ed.vertices[1]].append(ed.vertices[0])
1512 # Get groups of verts forming each face
1513 faces_verts_idx = []
1514 for v1 in related_key_verts: # verts-1 ....
1515 for v2 in related_key_verts: # verts-2
1516 if v1 != v2:
1517 related_verts_in_common = []
1518 v2_in_rel_v1 = False
1519 v1_in_rel_v2 = False
1520 for rel_v1 in related_key_verts[v1]:
1521 # Check if related verts of verts-1 are related verts of verts-2
1522 if rel_v1 in related_key_verts[v2]:
1523 related_verts_in_common.append(rel_v1)
1525 if v2 in related_key_verts[v1]:
1526 v2_in_rel_v1 = True
1528 if v1 in related_key_verts[v2]:
1529 v1_in_rel_v2 = True
1531 repeated_face = False
1532 # If two verts have two related verts in common, they form a quad
1533 if len(related_verts_in_common) == 2:
1534 # Check if the face is already saved
1535 for f_verts in faces_verts_idx:
1536 repeated_verts = 0
1538 if len(f_verts) == 4:
1539 if v1 in f_verts:
1540 repeated_verts += 1
1541 if v2 in f_verts:
1542 repeated_verts += 1
1543 if related_verts_in_common[0] in f_verts:
1544 repeated_verts += 1
1545 if related_verts_in_common[1] in f_verts:
1546 repeated_verts += 1
1548 if repeated_verts == len(f_verts):
1549 repeated_face = True
1550 break
1552 if not repeated_face:
1553 faces_verts_idx.append([v1, related_verts_in_common[0],
1554 v2, related_verts_in_common[1]])
1556 # If Two verts have one related vert in common and they are
1557 # related to each other, they form a triangle
1558 elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
1559 # Check if the face is already saved.
1560 for f_verts in faces_verts_idx:
1561 repeated_verts = 0
1563 if len(f_verts) == 3:
1564 if v1 in f_verts:
1565 repeated_verts += 1
1566 if v2 in f_verts:
1567 repeated_verts += 1
1568 if related_verts_in_common[0] in f_verts:
1569 repeated_verts += 1
1571 if repeated_verts == len(f_verts):
1572 repeated_face = True
1573 break
1575 if not repeated_face:
1576 faces_verts_idx.append([v1, related_verts_in_common[0], v2])
1578 # Keep only the faces that don't overlap by ignoring
1579 # quads that overlap with two adjacent triangles
1580 faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
1581 for i in range(len(faces_verts_idx)):
1582 for t in range(len(faces_verts_idx)):
1583 if i != t:
1584 verts_in_common = 0
1586 if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
1587 for v_idx in faces_verts_idx[t]:
1588 if v_idx in faces_verts_idx[i]:
1589 verts_in_common += 1
1590 # If it doesn't have all it's vertices repeated in the other face
1591 if verts_in_common == 3:
1592 if i not in faces_to_not_include_idx:
1593 faces_to_not_include_idx.append(i)
1595 # Build surface
1596 all_surface_verts_co = []
1597 for i in range(len(final_points_ob.data.vertices)):
1598 coords = final_points_ob.data.vertices[i].co
1599 all_surface_verts_co.append([coords[0], coords[1], coords[2]])
1601 # Verts of each face.
1602 all_surface_faces = []
1603 for i in range(len(faces_verts_idx)):
1604 if i not in faces_to_not_include_idx:
1605 face = []
1606 for v_idx in faces_verts_idx[i]:
1607 face.append(v_idx)
1609 all_surface_faces.append(face)
1611 # Build the mesh
1612 surf_me_name = "SURFSKIO_surface"
1613 me_surf = bpy.data.meshes.new(surf_me_name)
1614 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
1615 ob_surface = object_utils.object_data_add(context, me_surf)
1616 ob_surface.location = (0.0, 0.0, 0.0)
1617 ob_surface.rotation_euler = (0.0, 0.0, 0.0)
1618 ob_surface.scale = (1.0, 1.0, 1.0)
1620 # Delete final points temporal object
1621 bpy.ops.object.delete({"selected_objects": [final_points_ob]})
1623 # Delete isolated verts if there are any
1624 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1625 ob_surface.select_set(True)
1626 bpy.context.view_layer.objects.active = ob_surface
1628 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1629 bpy.ops.mesh.select_all(action='DESELECT')
1630 bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
1631 bpy.ops.mesh.delete()
1632 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1634 # Join crosshatch results with original mesh
1636 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1637 edges_length_sum = 0
1638 for ed in ob_surface.data.edges:
1639 edges_length_sum += (
1640 ob_surface.data.vertices[ed.vertices[0]].co -
1641 ob_surface.data.vertices[ed.vertices[1]].co
1642 ).length
1644 # Make dictionary with all the verts connected to each vert, on the new surface object.
1645 surface_connected_verts = {}
1646 for ed in ob_surface.data.edges:
1647 if not ed.vertices[0] in surface_connected_verts:
1648 surface_connected_verts[ed.vertices[0]] = []
1650 surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
1652 if ed.vertices[1] not in surface_connected_verts:
1653 surface_connected_verts[ed.vertices[1]] = []
1655 surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
1657 # Duplicate the new surface object, and use shrinkwrap to
1658 # calculate later the nearest verts to the main object
1659 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1660 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
1661 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1663 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
1665 final_ob_duplicate = bpy.context.view_layer.objects.active
1667 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1668 shrinkwrap_modifier = final_ob_duplicate.modifiers[-1]
1669 shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
1670 shrinkwrap_modifier.target = self.main_object
1672 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier=shrinkwrap_modifier.name)
1674 # Make list with verts of original mesh as index and coords as value
1675 main_object_verts_coords = []
1676 for v in self.main_object.data.vertices:
1677 coords = self.main_object.matrix_world @ v.co
1679 # To avoid problems when taking "-0.00" as a different value as "0.00"
1680 for c in range(len(coords)):
1681 if "%.3f" % coords[c] == "-0.00":
1682 coords[c] = 0
1684 main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
1686 tuple(main_object_verts_coords)
1688 # Determine which verts will be merged, snap them to the nearest verts
1689 # on the original verts, and get them selected
1690 crosshatch_verts_to_merge = []
1691 if self.automatic_join:
1692 for i in range(len(ob_surface.data.vertices)-1):
1693 # Calculate the distance from each of the connected verts to the actual vert,
1694 # and compare it with the distance they would have if joined.
1695 # If they don't change much, that vert can be joined
1696 merge_actual_vert = True
1697 try:
1698 if len(surface_connected_verts[i]) < 4:
1699 for c_v_idx in surface_connected_verts[i]:
1700 points_original = []
1701 points_original.append(ob_surface.data.vertices[c_v_idx].co)
1702 points_original.append(ob_surface.data.vertices[i].co)
1704 points_target = []
1705 points_target.append(ob_surface.data.vertices[c_v_idx].co)
1706 points_target.append(final_ob_duplicate.data.vertices[i].co)
1708 vec_A = points_original[0] - points_original[1]
1709 vec_B = points_target[0] - points_target[1]
1711 dist_A = (points_original[0] - points_original[1]).length
1712 dist_B = (points_target[0] - points_target[1]).length
1714 if not (
1715 points_original[0] == points_original[1] or
1716 points_target[0] == points_target[1]
1717 ): # If any vector's length is zero
1719 angle = vec_A.angle(vec_B) / pi
1720 else:
1721 angle = 0
1723 # Set a range of acceptable variation in the connected edges
1724 if dist_B > dist_A * 1.7 * self.join_stretch_factor or \
1725 dist_B < dist_A / 2 / self.join_stretch_factor or \
1726 angle >= 0.15 * self.join_stretch_factor:
1728 merge_actual_vert = False
1729 break
1730 else:
1731 merge_actual_vert = False
1732 except:
1733 self.report({'WARNING'},
1734 "Crosshatch set incorrectly")
1736 if merge_actual_vert:
1737 coords = final_ob_duplicate.data.vertices[i].co
1738 # To avoid problems when taking "-0.000" as a different value as "0.00"
1739 for c in range(len(coords)):
1740 if "%.3f" % coords[c] == "-0.00":
1741 coords[c] = 0
1743 comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
1745 if comparison_coords in main_object_verts_coords:
1746 # Get the index of the vert with those coords in the main object
1747 main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords)
1749 if self.main_object.data.vertices[main_object_related_vert_idx].select is True or \
1750 self.main_object_selected_verts_count == 0:
1752 ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
1753 ob_surface.data.vertices[i].select = True
1754 crosshatch_verts_to_merge.append(i)
1756 # Make sure the vert in the main object is selected,
1757 # in case it wasn't selected and the "join crosshatch" option is active
1758 self.main_object.data.vertices[main_object_related_vert_idx].select = True
1760 # Delete duplicated object
1761 bpy.ops.object.delete({"selected_objects": [final_ob_duplicate]})
1763 # Join crosshatched surface and main object
1764 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1765 ob_surface.select_set(True)
1766 self.main_object.select_set(True)
1767 bpy.context.view_layer.objects.active = self.main_object
1769 bpy.ops.object.join('INVOKE_REGION_WIN')
1771 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1772 # Perform Remove doubles to merge verts
1773 if not (self.automatic_join is False and self.main_object_selected_verts_count == 0):
1774 bpy.ops.mesh.remove_doubles(threshold=0.0001)
1776 bpy.ops.mesh.select_all(action='DESELECT')
1778 # If the main object has modifiers, turn their "viewport view status"
1779 # to what it was before the forced deactivation above
1780 if len(self.main_object.modifiers) > 0:
1781 for m_idx in range(len(self.main_object.modifiers)):
1782 self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
1784 self.update()
1786 return {'FINISHED'}
1788 def rectangular_surface(self, context):
1789 # Selected edges
1790 all_selected_edges_idx = []
1791 all_selected_verts = []
1792 all_verts_idx = []
1793 for ed in self.main_object.data.edges:
1794 if ed.select:
1795 all_selected_edges_idx.append(ed.index)
1797 # Selected vertices
1798 if not ed.vertices[0] in all_selected_verts:
1799 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
1800 if not ed.vertices[1] in all_selected_verts:
1801 all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
1803 # All verts (both from each edge) to determine later
1804 # which are at the tips (those not repeated twice)
1805 all_verts_idx.append(ed.vertices[0])
1806 all_verts_idx.append(ed.vertices[1])
1808 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1809 all_chains_tips_idx = []
1810 for v_idx in all_verts_idx:
1811 if all_verts_idx.count(v_idx) < 2:
1812 all_chains_tips_idx.append(v_idx)
1814 edges_connected_to_tips = []
1815 for ed in self.main_object.data.edges:
1816 if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and \
1817 not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
1819 edges_connected_to_tips.append(ed)
1821 # Check closed selections
1822 # List with groups of three verts, where the first element of the pair is
1823 # the unselected vert of a closed selection and the other two elements are the
1824 # selected neighbor verts (it will be useful to determine which selection chain
1825 # the unselected vert belongs to, and determine the "middle-vertex")
1826 single_unselected_verts_and_neighbors = []
1828 # To identify a "closed" selection (a selection that is a closed chain except
1829 # for one vertex) find the vertex in common that have the edges connected to tips.
1830 # If there is a vertex in common, that one is the unselected vert that closes
1831 # the selection or is a "middle-vertex"
1832 single_unselected_verts = []
1833 for ed in edges_connected_to_tips:
1834 for ed_b in edges_connected_to_tips:
1835 if ed != ed_b:
1836 if ed.vertices[0] == ed_b.vertices[0] and \
1837 not self.main_object.data.vertices[ed.vertices[0]].select and \
1838 ed.vertices[0] not in single_unselected_verts:
1840 # The second element is one of the tips of the selected
1841 # vertices of the closed selection
1842 single_unselected_verts_and_neighbors.append(
1843 [ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]
1845 single_unselected_verts.append(ed.vertices[0])
1846 break
1847 elif ed.vertices[0] == ed_b.vertices[1] and \
1848 not self.main_object.data.vertices[ed.vertices[0]].select and \
1849 ed.vertices[0] not in single_unselected_verts:
1851 single_unselected_verts_and_neighbors.append(
1852 [ed.vertices[0], ed.vertices[1], ed_b.vertices[0]]
1854 single_unselected_verts.append(ed.vertices[0])
1855 break
1856 elif ed.vertices[1] == ed_b.vertices[0] and \
1857 not self.main_object.data.vertices[ed.vertices[1]].select and \
1858 ed.vertices[1] not in single_unselected_verts:
1860 single_unselected_verts_and_neighbors.append(
1861 [ed.vertices[1], ed.vertices[0], ed_b.vertices[1]]
1863 single_unselected_verts.append(ed.vertices[1])
1864 break
1865 elif ed.vertices[1] == ed_b.vertices[1] and \
1866 not self.main_object.data.vertices[ed.vertices[1]].select and \
1867 ed.vertices[1] not in single_unselected_verts:
1869 single_unselected_verts_and_neighbors.append(
1870 [ed.vertices[1], ed.vertices[0], ed_b.vertices[0]]
1872 single_unselected_verts.append(ed.vertices[1])
1873 break
1875 middle_vertex_idx = None
1876 tips_to_discard_idx = []
1878 # Check if there is a "middle-vertex", and get its index
1879 for i in range(0, len(single_unselected_verts_and_neighbors)):
1880 actual_chain_verts = self.get_ordered_verts(
1881 self.main_object, all_selected_edges_idx,
1882 all_verts_idx, single_unselected_verts_and_neighbors[i][1],
1883 None, None
1886 if single_unselected_verts_and_neighbors[i][2] != \
1887 actual_chain_verts[len(actual_chain_verts) - 1].index:
1889 middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
1890 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
1891 tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
1893 # List with pairs of verts that belong to the tips of each selection chain (row)
1894 verts_tips_same_chain_idx = []
1895 if len(all_chains_tips_idx) >= 2:
1896 checked_v = []
1897 for i in range(0, len(all_chains_tips_idx)):
1898 if all_chains_tips_idx[i] not in checked_v:
1899 v_chain = self.get_ordered_verts(
1900 self.main_object, all_selected_edges_idx,
1901 all_verts_idx, all_chains_tips_idx[i],
1902 middle_vertex_idx, None
1905 verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
1907 checked_v.append(v_chain[0].index)
1908 checked_v.append(v_chain[len(v_chain) - 1].index)
1910 # Selection tips (vertices).
1911 verts_tips_parsed_idx = []
1912 if len(all_chains_tips_idx) >= 2:
1913 for spec_v_idx in all_chains_tips_idx:
1914 if (spec_v_idx not in tips_to_discard_idx):
1915 verts_tips_parsed_idx.append(spec_v_idx)
1917 # Identify the type of selection made by the user
1918 if middle_vertex_idx is not None:
1919 # If there are 4 tips (two selection chains), and
1920 # there is only one single unselected vert (the middle vert)
1921 if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1:
1922 selection_type = "TWO_CONNECTED"
1923 else:
1924 # The type of the selection was not identified, the script stops.
1925 self.report({'WARNING'}, "The selection isn't valid.")
1927 self.stopping_errors = True
1929 return{'CANCELLED'}
1930 else:
1931 if len(all_chains_tips_idx) == 2: # If there are 2 tips
1932 selection_type = "SINGLE"
1933 elif len(all_chains_tips_idx) == 4: # If there are 4 tips
1934 selection_type = "TWO_NOT_CONNECTED"
1935 elif len(all_chains_tips_idx) == 0:
1936 if len(self.main_splines.data.splines) > 1:
1937 selection_type = "NO_SELECTION"
1938 else:
1939 # If the selection was not identified and there is only one stroke,
1940 # there's no possibility to build a surface, so the script is interrupted
1941 self.report({'WARNING'}, "The selection isn't valid.")
1943 self.stopping_errors = True
1945 return{'CANCELLED'}
1946 else:
1947 # The type of the selection was not identified, the script stops
1948 self.report({'WARNING'}, "The selection isn't valid.")
1950 self.stopping_errors = True
1952 return{'CANCELLED'}
1954 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1955 if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
1956 self.report({'WARNING'},
1957 "At least two strokes are needed when there are two not connected selections")
1959 self.stopping_errors = True
1961 return{'CANCELLED'}
1963 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1965 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
1966 self.main_splines.select_set(True)
1967 bpy.context.view_layer.objects.active = self.main_splines
1969 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1970 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1971 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1972 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1973 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1974 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1975 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1976 bpy.ops.curve.smooth('INVOKE_REGION_WIN')
1977 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1979 self.selection_U_exists = False
1980 self.selection_U2_exists = False
1981 self.selection_V_exists = False
1982 self.selection_V2_exists = False
1984 self.selection_U_is_closed = False
1985 self.selection_U2_is_closed = False
1986 self.selection_V_is_closed = False
1987 self.selection_V2_is_closed = False
1989 # Define what vertices are at the tips of each selection and are not the middle-vertex
1990 if selection_type == "TWO_CONNECTED":
1991 self.selection_U_exists = True
1992 self.selection_V_exists = True
1994 closing_vert_U_idx = None
1995 closing_vert_V_idx = None
1996 closing_vert_U2_idx = None
1997 closing_vert_V2_idx = None
1999 # Determine which selection is Selection-U and which is Selection-V
2000 points_A = []
2001 points_B = []
2002 points_first_stroke_tips = []
2004 points_A.append(
2005 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[0]].co
2007 points_A.append(
2008 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
2010 points_B.append(
2011 self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[1]].co
2013 points_B.append(
2014 self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
2016 points_first_stroke_tips.append(
2017 self.main_splines.data.splines[0].bezier_points[0].co
2019 points_first_stroke_tips.append(
2020 self.main_splines.data.splines[0].bezier_points[
2021 len(self.main_splines.data.splines[0].bezier_points) - 1
2022 ].co
2025 angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
2026 angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
2028 if angle_A < angle_B:
2029 first_vert_U_idx = verts_tips_parsed_idx[0]
2030 first_vert_V_idx = verts_tips_parsed_idx[1]
2031 else:
2032 first_vert_U_idx = verts_tips_parsed_idx[1]
2033 first_vert_V_idx = verts_tips_parsed_idx[0]
2035 elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
2036 first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
2037 last_sketched_point_first_stroke_co = \
2038 self.main_splines.data.splines[0].bezier_points[
2039 len(self.main_splines.data.splines[0].bezier_points) - 1
2040 ].co
2041 first_sketched_point_last_stroke_co = \
2042 self.main_splines.data.splines[
2043 len(self.main_splines.data.splines) - 1
2044 ].bezier_points[0].co
2045 if len(self.main_splines.data.splines) > 1:
2046 first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
2047 last_sketched_point_second_stroke_co = \
2048 self.main_splines.data.splines[1].bezier_points[
2049 len(self.main_splines.data.splines[1].bezier_points) - 1
2050 ].co
2052 single_unselected_neighbors = [] # Only the neighbors of the single unselected verts
2053 for verts_neig_idx in single_unselected_verts_and_neighbors:
2054 single_unselected_neighbors.append(verts_neig_idx[1])
2055 single_unselected_neighbors.append(verts_neig_idx[2])
2057 all_chains_tips_and_middle_vert = []
2058 for v_idx in all_chains_tips_idx:
2059 if v_idx not in single_unselected_neighbors:
2060 all_chains_tips_and_middle_vert.append(v_idx)
2062 all_chains_tips_and_middle_vert += single_unselected_verts
2064 all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
2066 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2067 nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = \
2068 self.shortest_distance(
2069 self.main_object,
2070 first_sketched_point_first_stroke_co,
2071 all_chains_tips_and_middle_vert
2073 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2074 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2075 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2077 nearest_tip_to_first_st_first_pt_opposite_idx = \
2078 self.opposite_tip(
2079 nearest_tip_to_first_st_first_pt_idx,
2080 verts_tips_same_chain_idx
2082 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2083 nearest_tip_to_first_st_last_pt_idx, _temp_dist = \
2084 self.shortest_distance(
2085 self.main_object,
2086 last_sketched_point_first_stroke_co,
2087 all_chains_tips_and_middle_vert
2089 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2090 nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = \
2091 self.shortest_distance(
2092 self.main_object,
2093 first_sketched_point_last_stroke_co,
2094 all_chains_tips_and_middle_vert
2096 if len(self.main_splines.data.splines) > 1:
2097 # The selected vertex nearest to the first point of the second sketched stroke
2098 # (This will be useful to determine the direction of the closed
2099 # selection V when extruding along strokes)
2100 nearest_vert_to_second_st_first_pt_idx, _temp_dist = \
2101 self.shortest_distance(
2102 self.main_object,
2103 first_sketched_point_second_stroke_co,
2104 all_verts_idx
2106 # The selected vertex nearest to the first point of the second sketched stroke
2107 # (This will be useful to determine the direction of the closed
2108 # selection V2 when extruding along strokes)
2109 nearest_vert_to_second_st_last_pt_idx, _temp_dist = \
2110 self.shortest_distance(
2111 self.main_object,
2112 last_sketched_point_second_stroke_co,
2113 all_verts_idx
2115 # Determine if the single selection will be treated as U or as V
2116 edges_sum = 0
2117 for i in all_selected_edges_idx:
2118 edges_sum += (
2119 (self.main_object.matrix_world @
2120 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) -
2121 (self.main_object.matrix_world @
2122 self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)
2123 ).length
2125 average_edge_length = edges_sum / len(all_selected_edges_idx)
2127 # Get shortest distance from the first point of the last stroke to any participating vertex
2128 _temp_idx, shortest_distance_to_last_stroke = \
2129 self.shortest_distance(
2130 self.main_object,
2131 first_sketched_point_last_stroke_co,
2132 all_participating_verts
2134 # If the beginning of the first stroke is near enough, and its orientation
2135 # difference with the first edge of the nearest selection chain is not too high,
2136 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2137 if shortest_distance_to_first_stroke < average_edge_length / 4 and \
2138 shortest_distance_to_last_stroke < average_edge_length and \
2139 len(self.main_splines.data.splines) > 1:
2141 self.selection_U_exists = False
2142 self.selection_V_exists = True
2143 # If the first selection is not closed
2144 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2145 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2146 self.selection_V_is_closed = False
2147 closing_vert_U_idx = None
2148 closing_vert_U2_idx = None
2149 closing_vert_V_idx = None
2150 closing_vert_V2_idx = None
2152 first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2154 if selection_type == "TWO_NOT_CONNECTED":
2155 self.selection_V2_exists = True
2157 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2158 else:
2159 self.selection_V_is_closed = True
2160 closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
2162 # Get the neighbors of the first (unselected) vert of the closed selection U.
2163 vert_neighbors = []
2164 for verts in single_unselected_verts_and_neighbors:
2165 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2166 vert_neighbors.append(verts[1])
2167 vert_neighbors.append(verts[2])
2168 break
2170 verts_V = self.get_ordered_verts(
2171 self.main_object, all_selected_edges_idx,
2172 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2175 for i in range(0, len(verts_V)):
2176 if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
2177 # If the vertex nearest to the first point of the second stroke
2178 # is in the first half of the selected verts
2179 if i >= len(verts_V) / 2:
2180 first_vert_V_idx = vert_neighbors[1]
2181 break
2182 else:
2183 first_vert_V_idx = vert_neighbors[0]
2184 break
2186 if selection_type == "TWO_NOT_CONNECTED":
2187 self.selection_V2_exists = True
2188 # If the second selection is not closed
2189 if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or \
2190 nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx:
2192 self.selection_V2_is_closed = False
2193 closing_vert_V2_idx = None
2194 first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2196 else:
2197 self.selection_V2_is_closed = True
2198 closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
2200 # Get the neighbors of the first (unselected) vert of the closed selection U
2201 vert_neighbors = []
2202 for verts in single_unselected_verts_and_neighbors:
2203 if verts[0] == nearest_tip_to_first_st_last_pt_idx:
2204 vert_neighbors.append(verts[1])
2205 vert_neighbors.append(verts[2])
2206 break
2208 verts_V2 = self.get_ordered_verts(
2209 self.main_object, all_selected_edges_idx,
2210 all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
2213 for i in range(0, len(verts_V2)):
2214 if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
2215 # If the vertex nearest to the first point of the second stroke
2216 # is in the first half of the selected verts
2217 if i >= len(verts_V2) / 2:
2218 first_vert_V2_idx = vert_neighbors[1]
2219 break
2220 else:
2221 first_vert_V2_idx = vert_neighbors[0]
2222 break
2223 else:
2224 self.selection_V2_exists = False
2226 else:
2227 self.selection_U_exists = True
2228 self.selection_V_exists = False
2229 # If the first selection is not closed
2230 if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
2231 nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
2232 self.selection_U_is_closed = False
2233 closing_vert_U_idx = None
2235 points_tips = []
2236 points_tips.append(
2237 self.main_object.matrix_world @
2238 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2240 points_tips.append(
2241 self.main_object.matrix_world @
2242 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co
2244 points_first_stroke_tips = []
2245 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2246 points_first_stroke_tips.append(
2247 self.main_splines.data.splines[0].bezier_points[
2248 len(self.main_splines.data.splines[0].bezier_points) - 1
2249 ].co
2251 vec_A = points_tips[0] - points_tips[1]
2252 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2254 # Compare the direction of the selection and the first
2255 # grease pencil stroke to determine which is the "first" vertex of the selection
2256 if vec_A.dot(vec_B) < 0:
2257 first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
2258 else:
2259 first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2261 else:
2262 self.selection_U_is_closed = True
2263 closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
2265 # Get the neighbors of the first (unselected) vert of the closed selection U
2266 vert_neighbors = []
2267 for verts in single_unselected_verts_and_neighbors:
2268 if verts[0] == nearest_tip_to_first_st_first_pt_idx:
2269 vert_neighbors.append(verts[1])
2270 vert_neighbors.append(verts[2])
2271 break
2273 points_first_and_neighbor = []
2274 points_first_and_neighbor.append(
2275 self.main_object.matrix_world @
2276 self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
2278 points_first_and_neighbor.append(
2279 self.main_object.matrix_world @
2280 self.main_object.data.vertices[vert_neighbors[0]].co
2282 points_first_stroke_tips = []
2283 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
2284 points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
2286 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2287 vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
2289 # Compare the direction of the selection and the first grease pencil stroke to
2290 # determine which is the vertex neighbor to the first vertex (unselected) of
2291 # the closed selection. This will determine the direction of the closed selection
2292 if vec_A.dot(vec_B) < 0:
2293 first_vert_U_idx = vert_neighbors[1]
2294 else:
2295 first_vert_U_idx = vert_neighbors[0]
2297 if selection_type == "TWO_NOT_CONNECTED":
2298 self.selection_U2_exists = True
2299 # If the second selection is not closed
2300 if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or \
2301 nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx:
2303 self.selection_U2_is_closed = False
2304 closing_vert_U2_idx = None
2305 first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2306 else:
2307 self.selection_U2_is_closed = True
2308 closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
2310 # Get the neighbors of the first (unselected) vert of the closed selection U
2311 vert_neighbors = []
2312 for verts in single_unselected_verts_and_neighbors:
2313 if verts[0] == nearest_tip_to_last_st_first_pt_idx:
2314 vert_neighbors.append(verts[1])
2315 vert_neighbors.append(verts[2])
2316 break
2318 points_first_and_neighbor = []
2319 points_first_and_neighbor.append(
2320 self.main_object.matrix_world @
2321 self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co
2323 points_first_and_neighbor.append(
2324 self.main_object.matrix_world @
2325 self.main_object.data.vertices[vert_neighbors[0]].co
2327 points_last_stroke_tips = []
2328 points_last_stroke_tips.append(
2329 self.main_splines.data.splines[
2330 len(self.main_splines.data.splines) - 1
2331 ].bezier_points[0].co
2333 points_last_stroke_tips.append(
2334 self.main_splines.data.splines[
2335 len(self.main_splines.data.splines) - 1
2336 ].bezier_points[1].co
2338 vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
2339 vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
2341 # Compare the direction of the selection and the last grease pencil stroke to
2342 # determine which is the vertex neighbor to the first vertex (unselected) of
2343 # the closed selection. This will determine the direction of the closed selection
2344 if vec_A.dot(vec_B) < 0:
2345 first_vert_U2_idx = vert_neighbors[1]
2346 else:
2347 first_vert_U2_idx = vert_neighbors[0]
2348 else:
2349 self.selection_U2_exists = False
2351 elif selection_type == "NO_SELECTION":
2352 self.selection_U_exists = False
2353 self.selection_V_exists = False
2355 # Get an ordered list of the vertices of Selection-U
2356 verts_ordered_U = []
2357 if self.selection_U_exists:
2358 verts_ordered_U = self.get_ordered_verts(
2359 self.main_object, all_selected_edges_idx,
2360 all_verts_idx, first_vert_U_idx,
2361 middle_vertex_idx, closing_vert_U_idx
2364 # Get an ordered list of the vertices of Selection-U2
2365 verts_ordered_U2 = []
2366 if self.selection_U2_exists:
2367 verts_ordered_U2 = self.get_ordered_verts(
2368 self.main_object, all_selected_edges_idx,
2369 all_verts_idx, first_vert_U2_idx,
2370 middle_vertex_idx, closing_vert_U2_idx
2373 # Get an ordered list of the vertices of Selection-V
2374 verts_ordered_V = []
2375 if self.selection_V_exists:
2376 verts_ordered_V = self.get_ordered_verts(
2377 self.main_object, all_selected_edges_idx,
2378 all_verts_idx, first_vert_V_idx,
2379 middle_vertex_idx, closing_vert_V_idx
2381 verts_ordered_V_indices = [x.index for x in verts_ordered_V]
2383 # Get an ordered list of the vertices of Selection-V2
2384 verts_ordered_V2 = []
2385 if self.selection_V2_exists:
2386 verts_ordered_V2 = self.get_ordered_verts(
2387 self.main_object, all_selected_edges_idx,
2388 all_verts_idx, first_vert_V2_idx,
2389 middle_vertex_idx, closing_vert_V2_idx
2392 # Check if when there are two-not-connected selections both have the same
2393 # number of verts. If not terminate the script
2394 if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or
2395 (self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
2396 # Display a warning
2397 self.report({'WARNING'}, "Both selections must have the same number of edges")
2399 self.stopping_errors = True
2401 return{'CANCELLED'}
2403 # Calculate edges U proportions
2404 # Sum selected edges U lengths
2405 edges_lengths_U = []
2406 edges_lengths_sum_U = 0
2408 if self.selection_U_exists:
2409 edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(
2410 self.main_object,
2411 verts_ordered_U
2413 if self.selection_U2_exists:
2414 edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(
2415 self.main_object,
2416 verts_ordered_U2
2418 # Sum selected edges V lengths
2419 edges_lengths_V = []
2420 edges_lengths_sum_V = 0
2422 if self.selection_V_exists:
2423 edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(
2424 self.main_object,
2425 verts_ordered_V
2427 if self.selection_V2_exists:
2428 edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(
2429 self.main_object,
2430 verts_ordered_V2
2433 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2434 bpy.ops.curve.subdivide('INVOKE_REGION_WIN',
2435 number_cuts=bpy.context.scene.bsurfaces.SURFSK_precision)
2436 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2438 # Proportions U
2439 edges_proportions_U = []
2440 edges_proportions_U = self.get_edges_proportions(
2441 edges_lengths_U, edges_lengths_sum_U,
2442 self.selection_U_exists, self.edges_U
2444 verts_count_U = len(edges_proportions_U) + 1
2446 if self.selection_U2_exists:
2447 edges_proportions_U2 = []
2448 edges_proportions_U2 = self.get_edges_proportions(
2449 edges_lengths_U2, edges_lengths_sum_U2,
2450 self.selection_U2_exists, self.edges_V
2453 # Proportions V
2454 edges_proportions_V = []
2455 edges_proportions_V = self.get_edges_proportions(
2456 edges_lengths_V, edges_lengths_sum_V,
2457 self.selection_V_exists, self.edges_V
2460 if self.selection_V2_exists:
2461 edges_proportions_V2 = []
2462 edges_proportions_V2 = self.get_edges_proportions(
2463 edges_lengths_V2, edges_lengths_sum_V2,
2464 self.selection_V2_exists, self.edges_V
2467 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2468 # the actual sketched curves with a "closing segment"
2469 if self.cyclic_follow and not self.selection_V_exists and not \
2470 ((self.selection_U_exists and not self.selection_U_is_closed) or
2471 (self.selection_U2_exists and not self.selection_U2_is_closed)):
2473 simplified_spline_coords = []
2474 simplified_curve = []
2475 ob_simplified_curve = []
2476 splines_first_v_co = []
2477 for i in range(len(self.main_splines.data.splines)):
2478 # Create a curve object for the actual spline "cyclic extension"
2479 simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
2480 ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
2481 bpy.context.collection.objects.link(ob_simplified_curve[i])
2483 simplified_curve[i].dimensions = "3D"
2485 spline_coords = []
2486 for bp in self.main_splines.data.splines[i].bezier_points:
2487 spline_coords.append(bp.co)
2489 # Simplification
2490 simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
2492 # Get the coordinates of the first vert of the actual spline
2493 splines_first_v_co.append(simplified_spline_coords[i][0])
2495 # Generate the spline
2496 spline = simplified_curve[i].splines.new('BEZIER')
2497 # less one because one point is added when the spline is created
2498 spline.bezier_points.add(len(simplified_spline_coords[i]) - 1)
2499 for p in range(0, len(simplified_spline_coords[i])):
2500 spline.bezier_points[p].co = simplified_spline_coords[i][p]
2502 spline.use_cyclic_u = True
2504 spline_bp_count = len(spline.bezier_points)
2506 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2507 ob_simplified_curve[i].select_set(True)
2508 bpy.context.view_layer.objects.active = ob_simplified_curve[i]
2510 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2511 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
2512 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2513 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
2514 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2516 # Select the "closing segment", and subdivide it
2517 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
2518 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
2519 ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
2521 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
2522 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
2523 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
2525 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2526 segments = sqrt(
2527 (ob_simplified_curve[i].data.splines[0].bezier_points[0].co -
2528 ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length /
2529 self.average_gp_segment_length
2531 for t in range(2):
2532 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=segments)
2534 # Delete the other vertices and make it non-cyclic to
2535 # keep only the needed verts of the "closing segment"
2536 bpy.ops.curve.select_all(action='INVERT')
2537 bpy.ops.curve.delete(type='VERT')
2538 ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
2539 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2541 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2542 first_new_index = len(self.main_splines.data.splines[i].bezier_points)
2543 self.main_splines.data.splines[i].bezier_points.add(
2544 len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1
2546 for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
2547 self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = \
2548 ob_simplified_curve[i].data.splines[0].bezier_points[t].co
2550 # Delete the temporal curve
2551 bpy.ops.object.delete({"selected_objects": [ob_simplified_curve[i]]})
2553 # Get the coords of the points distributed along the sketched strokes,
2554 # with proportions-U of the first selection
2555 pts_on_strokes_with_proportions_U = self.distribute_pts(
2556 self.main_splines.data.splines,
2557 edges_proportions_U
2559 sketched_splines_parsed = []
2561 if self.selection_U2_exists:
2562 # Initialize the multidimensional list with the proportions of all the segments
2563 proportions_loops_crossing_strokes = []
2564 for i in range(len(pts_on_strokes_with_proportions_U)):
2565 proportions_loops_crossing_strokes.append([])
2567 for t in range(len(pts_on_strokes_with_proportions_U[0])):
2568 proportions_loops_crossing_strokes[i].append(None)
2570 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2571 for lp in range(len(pts_on_strokes_with_proportions_U[0])):
2572 loop_segments_lengths = []
2574 for st in range(len(pts_on_strokes_with_proportions_U)):
2575 # When on the first stroke, add the segment from the selection to the dirst stroke
2576 if st == 0:
2577 loop_segments_lengths.append(
2578 ((self.main_object.matrix_world @ verts_ordered_U[lp].co) -
2579 pts_on_strokes_with_proportions_U[0][lp]).length
2581 # For all strokes except for the last, calculate the distance
2582 # from the actual stroke to the next
2583 if st != len(pts_on_strokes_with_proportions_U) - 1:
2584 loop_segments_lengths.append(
2585 (pts_on_strokes_with_proportions_U[st][lp] -
2586 pts_on_strokes_with_proportions_U[st + 1][lp]).length
2588 # When on the last stroke, add the segments
2589 # from the last stroke to the second selection
2590 if st == len(pts_on_strokes_with_proportions_U) - 1:
2591 loop_segments_lengths.append(
2592 (pts_on_strokes_with_proportions_U[st][lp] -
2593 (self.main_object.matrix_world @ verts_ordered_U2[lp].co)).length
2595 # Calculate full loop length
2596 loop_seg_lengths_sum = 0
2597 for i in range(len(loop_segments_lengths)):
2598 loop_seg_lengths_sum += loop_segments_lengths[i]
2600 # Fill the multidimensional list with the proportions of all the segments
2601 for st in range(len(pts_on_strokes_with_proportions_U)):
2602 proportions_loops_crossing_strokes[st][lp] = \
2603 loop_segments_lengths[st] / loop_seg_lengths_sum
2605 # Calculate proportions for each stroke
2606 for st in range(len(pts_on_strokes_with_proportions_U)):
2607 actual_stroke_spline = []
2608 # Needs to be a list for the "distribute_pts" method
2609 actual_stroke_spline.append(self.main_splines.data.splines[st])
2611 # Calculate the proportions for the actual stroke.
2612 actual_edges_proportions_U = []
2613 for i in range(len(edges_proportions_U)):
2614 proportions_sum = 0
2616 # Sum the proportions of this loop up to the actual.
2617 for t in range(0, st + 1):
2618 proportions_sum += proportions_loops_crossing_strokes[t][i]
2619 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2620 # and the proportions refer to edges, so we start at the element 1
2621 # of proportions_loops_crossing_strokes instead of element 0
2622 actual_edges_proportions_U.append(
2623 edges_proportions_U[i] -
2624 ((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)
2626 points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
2627 sketched_splines_parsed.append(points_actual_spline[0])
2628 else:
2629 sketched_splines_parsed = pts_on_strokes_with_proportions_U
2631 # If the selection type is "TWO_NOT_CONNECTED" replace the
2632 # points of the last spline with the points in the "target" selection
2633 if selection_type == "TWO_NOT_CONNECTED":
2634 if self.selection_U2_exists:
2635 for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
2636 sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = \
2637 self.main_object.matrix_world @ verts_ordered_U2[i].co
2639 # Create temporary curves along the "control-points" found
2640 # on the sketched curves and the mesh selection
2641 mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
2642 me = bpy.data.meshes.new(mesh_ctrl_pts_name)
2643 ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
2644 ob_ctrl_pts.data = me
2645 bpy.context.collection.objects.link(ob_ctrl_pts)
2647 cyclic_loops_U = []
2648 first_verts = []
2649 second_verts = []
2650 last_verts = []
2652 for i in range(0, verts_count_U):
2653 vert_num_in_spline = 1
2655 if self.selection_U_exists:
2656 ob_ctrl_pts.data.vertices.add(1)
2657 last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2658 last_v.co = self.main_object.matrix_world @ verts_ordered_U[i].co
2660 vert_num_in_spline += 1
2662 for t in range(0, len(sketched_splines_parsed)):
2663 ob_ctrl_pts.data.vertices.add(1)
2664 v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
2665 v.co = sketched_splines_parsed[t][i]
2667 if vert_num_in_spline > 1:
2668 ob_ctrl_pts.data.edges.add(1)
2669 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = \
2670 len(ob_ctrl_pts.data.vertices) - 2
2671 ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = \
2672 len(ob_ctrl_pts.data.vertices) - 1
2674 if t == 0:
2675 first_verts.append(v.index)
2677 if t == 1:
2678 second_verts.append(v.index)
2680 if t == len(sketched_splines_parsed) - 1:
2681 last_verts.append(v.index)
2683 last_v = v
2684 vert_num_in_spline += 1
2686 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2687 ob_ctrl_pts.select_set(True)
2688 bpy.context.view_layer.objects.active = ob_ctrl_pts
2690 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2691 bpy.ops.mesh.select_all(action='DESELECT')
2692 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2694 # Determine which loops-U will be "Cyclic"
2695 for i in range(0, len(first_verts)):
2696 # When there is Cyclic Cross there is no need of
2697 # Automatic Join, (and there are at least three strokes)
2698 if self.automatic_join and not self.cyclic_cross and \
2699 selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3:
2701 v = ob_ctrl_pts.data.vertices
2702 first_point_co = v[first_verts[i]].co
2703 second_point_co = v[second_verts[i]].co
2704 last_point_co = v[last_verts[i]].co
2706 # Coordinates of the point in the center of both the first and last verts.
2707 verts_center_co = [
2708 (first_point_co[0] + last_point_co[0]) / 2,
2709 (first_point_co[1] + last_point_co[1]) / 2,
2710 (first_point_co[2] + last_point_co[2]) / 2
2712 vec_A = second_point_co - first_point_co
2713 vec_B = second_point_co - Vector(verts_center_co)
2715 # Calculate the length of the first segment of the loop,
2716 # and the length it would have after moving the first vert
2717 # to the middle position between first and last
2718 length_original = (second_point_co - first_point_co).length
2719 length_target = (second_point_co - Vector(verts_center_co)).length
2721 angle = vec_A.angle(vec_B) / pi
2723 # If the target length doesn't stretch too much, and the
2724 # its angle doesn't change to much either
2725 if length_target <= length_original * 1.03 * self.join_stretch_factor and \
2726 angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists:
2728 cyclic_loops_U.append(True)
2729 # Move the first vert to the center coordinates
2730 ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
2731 # Select the last verts from Cyclic loops, for later deletion all at once
2732 v[last_verts[i]].select = True
2733 else:
2734 cyclic_loops_U.append(False)
2735 else:
2736 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2737 if self.cyclic_cross and not self.selection_U_exists and not \
2738 ((self.selection_V_exists and not self.selection_V_is_closed) or
2739 (self.selection_V2_exists and not self.selection_V2_is_closed)):
2741 cyclic_loops_U.append(True)
2742 else:
2743 cyclic_loops_U.append(False)
2745 # The cyclic_loops_U list needs to be reversed.
2746 cyclic_loops_U.reverse()
2748 # Delete the previously selected (last_)verts.
2749 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2750 bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
2751 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2753 # Create curves from control points.
2754 bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
2755 ob_curves_surf = bpy.context.view_layer.objects.active
2756 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2757 bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2758 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2760 # Make Cyclic the splines designated as Cyclic.
2761 for i in range(0, len(cyclic_loops_U)):
2762 ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
2764 # Get the coords of all points on first loop-U, for later comparison with its
2765 # subdivided version, to know which points of the loops-U are crossed by the
2766 # original strokes. The indices will be the same for the other loops-U
2767 if self.loops_on_strokes:
2768 coords_loops_U_control_points = []
2769 for p in ob_ctrl_pts.data.splines[0].bezier_points:
2770 coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
2772 tuple(coords_loops_U_control_points)
2774 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2775 if self.loops_on_strokes and not self.selection_V_exists:
2776 edges_V_count = len(self.main_splines.data.splines) * self.edges_V
2777 else:
2778 edges_V_count = len(edges_proportions_V)
2780 # The Follow precision will vary depending on the number of Follow face-loops
2781 precision_multiplier = round(2 + (edges_V_count / 15))
2782 curve_cuts = bpy.context.scene.bsurfaces.SURFSK_precision * precision_multiplier
2784 # Subdivide the curves
2785 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=curve_cuts)
2787 # The verts position shifting that happens with splines subdivision.
2788 # For later reorder splines points
2789 verts_position_shift = curve_cuts + 1
2790 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
2792 # Reorder coordinates of the points of each spline to put the first point of
2793 # the spline starting at the position it was the first point before sudividing
2794 # the curve. And make a new curve object per spline (to handle memory better later)
2795 splines_U_objects = []
2796 for i in range(len(ob_curves_surf.data.splines)):
2797 spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
2798 ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
2799 bpy.context.collection.objects.link(ob_spline_U)
2801 spline_U_curve.dimensions = "3D"
2803 # Add points to the spline in the new curve object
2804 ob_spline_U.data.splines.new('BEZIER')
2805 for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
2806 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2807 if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
2808 point_index = t + verts_position_shift
2809 else:
2810 point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
2811 else:
2812 point_index = t
2813 # to avoid adding the first point since it's added when the spline is created
2814 if t > 0:
2815 ob_spline_U.data.splines[0].bezier_points.add(1)
2816 ob_spline_U.data.splines[0].bezier_points[t].co = \
2817 ob_curves_surf.data.splines[i].bezier_points[point_index].co
2819 if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
2820 # Add a last point at the same location as the first one
2821 ob_spline_U.data.splines[0].bezier_points.add(1)
2822 ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = \
2823 ob_spline_U.data.splines[0].bezier_points[0].co
2824 else:
2825 ob_spline_U.data.splines[0].use_cyclic_u = False
2827 splines_U_objects.append(ob_spline_U)
2828 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
2829 ob_spline_U.select_set(True)
2830 bpy.context.view_layer.objects.active = ob_spline_U
2832 # When option "Loops on strokes" is active each "Cross" loop will have
2833 # its own proportions according to where the original strokes "touch" them
2834 if self.loops_on_strokes:
2835 # Get the indices of points where the original strokes "touch" loops-U
2836 points_U_crossed_by_strokes = []
2837 for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
2838 bp = splines_U_objects[0].data.splines[0].bezier_points[i]
2839 if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
2840 points_U_crossed_by_strokes.append(i)
2842 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2843 edge_order_number_for_splines = {}
2844 if self.selection_V_exists:
2845 # For two-connected selections add a first hypothetic stroke at the beginning.
2846 if selection_type == "TWO_CONNECTED":
2847 edge_order_number_for_splines[0] = 0
2849 for i in range(len(self.main_splines.data.splines)):
2850 sp = self.main_splines.data.splines[i]
2851 v_idx, _dist_temp = self.shortest_distance(
2852 self.main_object,
2853 sp.bezier_points[0].co,
2854 verts_ordered_V_indices
2856 # Get the position (edges count) of the vert v_idx in the selected chain V
2857 edge_idx_in_chain = verts_ordered_V_indices.index(v_idx)
2859 # For two-connected selections the strokes go after the
2860 # hypothetic stroke added before, so the index adds one per spline
2861 if selection_type == "TWO_CONNECTED":
2862 spline_number = i + 1
2863 else:
2864 spline_number = i
2866 edge_order_number_for_splines[spline_number] = edge_idx_in_chain
2868 # Get the first and last verts indices for later comparison
2869 if i == 0:
2870 first_v_idx = v_idx
2871 elif i == len(self.main_splines.data.splines) - 1:
2872 last_v_idx = v_idx
2874 if self.selection_V_is_closed:
2875 # If there is no last stroke on the last vertex (same as first vertex),
2876 # add a hypothetic spline at last vert order
2877 if first_v_idx != last_v_idx:
2878 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2879 len(verts_ordered_V_indices) - 1
2880 else:
2881 if self.cyclic_cross:
2882 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2883 len(verts_ordered_V_indices) - 2
2884 edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
2885 len(verts_ordered_V_indices) - 1
2886 else:
2887 edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
2888 len(verts_ordered_V_indices) - 1
2890 # Get the coords of the points distributed along the
2891 # "crossing curves", with appropriate proportions-V
2892 surface_splines_parsed = []
2893 for i in range(len(splines_U_objects)):
2894 sp_ob = splines_U_objects[i]
2895 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2896 if self.loops_on_strokes:
2897 # Segments distances from stroke to stroke
2898 dist = 0
2899 full_dist = 0
2900 segments_distances = []
2901 for t in range(len(sp_ob.data.splines[0].bezier_points)):
2902 bp = sp_ob.data.splines[0].bezier_points[t]
2904 if t == 0:
2905 last_p = bp.co
2906 else:
2907 actual_p = bp.co
2908 dist += (last_p - actual_p).length
2910 if t in points_U_crossed_by_strokes:
2911 segments_distances.append(dist)
2912 full_dist += dist
2914 dist = 0
2916 last_p = actual_p
2918 # Calculate Proportions.
2919 used_edges_proportions_V = []
2920 for t in range(len(segments_distances)):
2921 if self.selection_V_exists:
2922 if t == 0:
2923 order_number_last_stroke = 0
2925 segment_edges_length_V = 0
2926 segment_edges_length_V2 = 0
2927 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2928 segment_edges_length_V += edges_lengths_V[order]
2929 if self.selection_V2_exists:
2930 segment_edges_length_V2 += edges_lengths_V2[order]
2932 for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
2933 # Calculate each "sub-segment" (the ones between each stroke) length
2934 if self.selection_V2_exists:
2935 proportion_sub_seg = (edges_lengths_V2[order] -
2936 ((edges_lengths_V2[order] - edges_lengths_V[order]) /
2937 len(splines_U_objects) * i)) / (segment_edges_length_V2 -
2938 (segment_edges_length_V2 - segment_edges_length_V) /
2939 len(splines_U_objects) * i)
2941 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2942 else:
2943 proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
2944 sub_seg_dist = segments_distances[t] * proportion_sub_seg
2946 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2948 order_number_last_stroke = edge_order_number_for_splines[t + 1]
2950 else:
2951 for _c in range(self.edges_V):
2952 # Calculate each "sub-segment" (the ones between each stroke) length
2953 sub_seg_dist = segments_distances[t] / self.edges_V
2954 used_edges_proportions_V.append(sub_seg_dist / full_dist)
2956 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2957 surface_splines_parsed.append(actual_spline[0])
2959 else:
2960 if self.selection_V2_exists:
2961 used_edges_proportions_V = []
2962 for p in range(len(edges_proportions_V)):
2963 used_edges_proportions_V.append(
2964 edges_proportions_V2[p] -
2965 ((edges_proportions_V2[p] -
2966 edges_proportions_V[p]) / len(splines_U_objects) * i)
2968 else:
2969 used_edges_proportions_V = edges_proportions_V
2971 actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
2972 surface_splines_parsed.append(actual_spline[0])
2974 # Set the verts of the first and last splines to the locations
2975 # of the respective verts in the selections
2976 if self.selection_V_exists:
2977 for i in range(0, len(surface_splines_parsed[0])):
2978 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = \
2979 self.main_object.matrix_world @ verts_ordered_V[i].co
2981 if selection_type == "TWO_NOT_CONNECTED":
2982 if self.selection_V2_exists:
2983 for i in range(0, len(surface_splines_parsed[0])):
2984 surface_splines_parsed[0][i] = self.main_object.matrix_world @ verts_ordered_V2[i].co
2986 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2987 # merge the verts of the tips of the loops when they are "near enough"
2988 if self.automatic_join and selection_type != "TWO_CONNECTED":
2989 # Join the tips of "Follow" loops that are near enough and must be "closed"
2990 if not self.selection_V_exists and len(edges_proportions_U) >= 3:
2991 for i in range(len(surface_splines_parsed[0])):
2992 sp = surface_splines_parsed
2993 loop_segment_dist = (sp[0][i] - sp[1][i]).length
2995 verts_middle_position_co = [
2996 (sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2,
2997 (sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2,
2998 (sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2
3000 points_original = []
3001 points_original.append(sp[1][i])
3002 points_original.append(sp[0][i])
3004 points_target = []
3005 points_target.append(sp[1][i])
3006 points_target.append(Vector(verts_middle_position_co))
3008 vec_A = points_original[0] - points_original[1]
3009 vec_B = points_target[0] - points_target[1]
3010 # check for zero angles, not sure if it is a great fix
3011 if vec_A.length != 0 and vec_B.length != 0:
3012 angle = vec_A.angle(vec_B) / pi
3013 edge_new_length = (Vector(verts_middle_position_co) - sp[1][i]).length
3014 else:
3015 angle = 0
3016 edge_new_length = 0
3018 # If after moving the verts to the middle point, the segment doesn't stretch too much
3019 if edge_new_length <= loop_segment_dist * 1.5 * \
3020 self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor:
3022 # Avoid joining when the actual loop must be merged with the original mesh
3023 if not (self.selection_U_exists and i == 0) and \
3024 not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1):
3026 # Change the coords of both verts to the middle position
3027 surface_splines_parsed[0][i] = verts_middle_position_co
3028 surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
3030 # Delete object with control points and object from grease pencil conversion
3031 bpy.ops.object.delete({"selected_objects": [ob_ctrl_pts]})
3033 bpy.ops.object.delete({"selected_objects": splines_U_objects})
3035 # Generate surface
3037 # Get all verts coords
3038 all_surface_verts_co = []
3039 for i in range(0, len(surface_splines_parsed)):
3040 # Get coords of all verts and make a list with them
3041 for pt_co in surface_splines_parsed[i]:
3042 all_surface_verts_co.append(pt_co)
3044 # Define verts for each face
3045 all_surface_faces = []
3046 for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
3047 if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
3048 all_surface_faces.append(
3049 [i + 1, i, i + len(surface_splines_parsed[0]),
3050 i + len(surface_splines_parsed[0]) + 1]
3052 # Build the mesh
3053 surf_me_name = "SURFSKIO_surface"
3054 me_surf = bpy.data.meshes.new(surf_me_name)
3055 me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
3056 ob_surface = object_utils.object_data_add(context, me_surf)
3057 ob_surface.location = (0.0, 0.0, 0.0)
3058 ob_surface.rotation_euler = (0.0, 0.0, 0.0)
3059 ob_surface.scale = (1.0, 1.0, 1.0)
3061 # Select all the "unselected but participating" verts, from closed selection
3062 # or double selections with middle-vertex, for later join with remove doubles
3063 for v_idx in single_unselected_verts:
3064 self.main_object.data.vertices[v_idx].select = True
3066 # Join the new mesh to the main object
3067 ob_surface.select_set(True)
3068 self.main_object.select_set(True)
3069 bpy.context.view_layer.objects.active = self.main_object
3071 bpy.ops.object.join('INVOKE_REGION_WIN')
3073 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3075 bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
3076 bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
3077 bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
3079 self.update()
3081 return{'FINISHED'}
3083 def update(self):
3084 try:
3085 global global_offset
3086 shrinkwrap = self.main_object.modifiers["Shrinkwrap"]
3087 shrinkwrap.offset = global_offset
3088 bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset = global_offset
3089 except:
3090 pass
3092 try:
3093 global global_color
3094 material = makeMaterial("BSurfaceMesh", global_color)
3095 if self.main_object.data.materials:
3096 self.main_object.data.materials[0] = material
3097 else:
3098 self.main_object.data.materials.append(material)
3099 bpy.context.scene.bsurfaces.SURFSK_mesh_color = global_color
3100 except:
3101 pass
3103 try:
3104 global global_in_front
3105 self.main_object.show_in_front = global_in_front
3106 bpy.context.scene.bsurfaces.SURFSK_in_front = global_in_front
3107 except:
3108 pass
3110 try:
3111 global global_show_wire
3112 self.main_object.show_wire = global_show_wire
3113 bpy.context.scene.bsurfaces.SURFSK_show_wire = global_show_wire
3114 except:
3115 pass
3117 try:
3118 global global_shade_smooth
3119 if global_shade_smooth:
3120 bpy.ops.object.shade_smooth()
3121 else:
3122 bpy.ops.object.shade_flat()
3123 bpy.context.scene.bsurfaces.SURFSK_shade_smooth = global_shade_smooth
3124 except:
3125 pass
3127 return{'FINISHED'}
3129 def execute(self, context):
3131 if bpy.ops.object.mode_set.poll():
3132 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3134 try:
3135 global global_mesh_object
3136 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3137 bpy.data.objects[global_mesh_object].select_set(True)
3138 self.main_object = bpy.data.objects[global_mesh_object]
3139 bpy.context.view_layer.objects.active = self.main_object
3140 bsurfaces_props = bpy.context.scene.bsurfaces
3141 except:
3142 self.report({'WARNING'}, "Specify the name of the object with retopology")
3143 return{"CANCELLED"}
3144 bpy.context.view_layer.objects.active = self.main_object
3146 self.update()
3148 if not self.is_fill_faces:
3149 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3150 value='True, False, False')
3152 # Build splines from the "last saved splines".
3153 last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
3154 self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
3155 bpy.context.collection.objects.link(self.main_splines)
3157 last_saved_curve.dimensions = "3D"
3159 for sp in self.last_strokes_splines_coords:
3160 spline = self.main_splines.data.splines.new('BEZIER')
3161 # less one because one point is added when the spline is created
3162 spline.bezier_points.add(len(sp) - 1)
3163 for p in range(0, len(sp)):
3164 spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
3166 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3168 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3169 self.main_splines.select_set(True)
3170 bpy.context.view_layer.objects.active = self.main_splines
3172 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3174 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3175 # Important to make it vector first and then automatic, otherwise the
3176 # tips handles get too big and distort the shrinkwrap results later
3177 bpy.ops.curve.handle_type_set(type='VECTOR')
3178 bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3179 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3180 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3182 self.main_splines.name = "SURFSKIO_temp_strokes"
3184 if self.is_crosshatch:
3185 strokes_for_crosshatch = True
3186 strokes_for_rectangular_surface = False
3187 else:
3188 strokes_for_rectangular_surface = True
3189 strokes_for_crosshatch = False
3191 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3193 if strokes_for_rectangular_surface:
3194 self.rectangular_surface(context)
3195 elif strokes_for_crosshatch:
3196 self.crosshatch_surface_execute(context)
3198 #Set Shade smooth to new polygons
3199 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3200 global global_shade_smooth
3201 if global_shade_smooth:
3202 bpy.ops.object.shade_smooth()
3203 else:
3204 bpy.ops.object.shade_flat()
3206 # Delete main splines
3207 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3208 if self.keep_strokes:
3209 self.main_splines.name = "keep_strokes"
3210 self.main_splines.data.bevel_depth = 0.001
3211 if "keep_strokes_material" in bpy.data.materials :
3212 self.main_splines.data.materials.append(bpy.data.materials["keep_strokes_material"])
3213 else:
3214 mat = bpy.data.materials.new("keep_strokes_material")
3215 mat.diffuse_color = (1, 0, 0, 0)
3216 mat.specular_color = (1, 0, 0)
3217 mat.specular_intensity = 0.0
3218 mat.roughness = 0.0
3219 self.main_splines.data.materials.append(mat)
3220 else:
3221 bpy.ops.object.delete({"selected_objects": [self.main_splines]})
3223 # Delete grease pencil strokes
3224 if self.strokes_type == "GP_STROKES" and not self.stopping_errors:
3225 try:
3226 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3227 except:
3228 pass
3230 # Delete annotations
3231 if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors:
3232 try:
3233 bpy.context.annotation_data.layers.active.clear()
3234 except:
3235 pass
3237 bsurfaces_props = bpy.context.scene.bsurfaces
3238 bsurfaces_props.SURFSK_edges_U = self.edges_U
3239 bsurfaces_props.SURFSK_edges_V = self.edges_V
3240 bsurfaces_props.SURFSK_cyclic_cross = self.cyclic_cross
3241 bsurfaces_props.SURFSK_cyclic_follow = self.cyclic_follow
3242 bsurfaces_props.SURFSK_automatic_join = self.automatic_join
3243 bsurfaces_props.SURFSK_loops_on_strokes = self.loops_on_strokes
3244 bsurfaces_props.SURFSK_keep_strokes = self.keep_strokes
3246 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3247 self.main_object.select_set(True)
3248 bpy.context.view_layer.objects.active = self.main_object
3250 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3252 self.update()
3254 return{'FINISHED'}
3256 def invoke(self, context, event):
3258 if bpy.ops.object.mode_set.poll():
3259 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3261 bsurfaces_props = bpy.context.scene.bsurfaces
3262 self.cyclic_cross = bsurfaces_props.SURFSK_cyclic_cross
3263 self.cyclic_follow = bsurfaces_props.SURFSK_cyclic_follow
3264 self.automatic_join = bsurfaces_props.SURFSK_automatic_join
3265 self.loops_on_strokes = bsurfaces_props.SURFSK_loops_on_strokes
3266 self.keep_strokes = bsurfaces_props.SURFSK_keep_strokes
3268 try:
3269 global global_mesh_object
3270 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3271 bpy.data.objects[global_mesh_object].select_set(True)
3272 self.main_object = bpy.data.objects[global_mesh_object]
3273 bpy.context.view_layer.objects.active = self.main_object
3274 except:
3275 self.report({'WARNING'}, "Specify the name of the object with retopology")
3276 return{"CANCELLED"}
3278 self.update()
3280 self.main_object_selected_verts_count = len([v for v in self.main_object.data.vertices if v.select])
3282 bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
3283 value='True, False, False')
3285 self.edges_U = bsurfaces_props.SURFSK_edges_U
3286 self.edges_V = bsurfaces_props.SURFSK_edges_V
3288 self.is_fill_faces = False
3289 self.stopping_errors = False
3290 self.last_strokes_splines_coords = []
3292 # Determine the type of the strokes
3293 self.strokes_type = get_strokes_type(context)
3295 # Check if it will be used grease pencil strokes or curves
3296 # If there are strokes to be used
3297 if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "GP_ANNOTATION":
3298 if self.strokes_type == "GP_STROKES":
3299 # Convert grease pencil strokes to curve
3300 global global_gpencil_object
3301 gp = bpy.data.objects[global_gpencil_object]
3302 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3303 self.using_external_curves = False
3305 elif self.strokes_type == "GP_ANNOTATION":
3306 # Convert grease pencil strokes to curve
3307 gp = bpy.context.annotation_data
3308 self.original_curve = conver_gpencil_to_curve(self, context, gp, 'Annotation')
3309 self.using_external_curves = False
3311 elif self.strokes_type == "EXTERNAL_CURVE":
3312 global global_curve_object
3313 self.original_curve = bpy.data.objects[global_curve_object]
3314 self.using_external_curves = True
3316 # Make sure there are no objects left from erroneous
3317 # executions of this operator, with the reserved names used here
3318 for o in bpy.data.objects:
3319 if o.name.find("SURFSKIO_") != -1:
3320 bpy.ops.object.delete({"selected_objects": [o]})
3322 bpy.context.view_layer.objects.active = self.original_curve
3324 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3326 self.temporary_curve = bpy.context.view_layer.objects.active
3328 # Deselect all points of the curve
3329 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3330 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3331 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3333 # Delete splines with only a single isolated point
3334 for i in range(len(self.temporary_curve.data.splines)):
3335 sp = self.temporary_curve.data.splines[i]
3337 if len(sp.bezier_points) == 1:
3338 sp.bezier_points[0].select_control_point = True
3340 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3341 bpy.ops.curve.delete(type='VERT')
3342 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3344 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3345 self.temporary_curve.select_set(True)
3346 bpy.context.view_layer.objects.active = self.temporary_curve
3348 # Set a minimum number of points for crosshatch
3349 minimum_points_num = 15
3351 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3352 # Check if the number of points of each curve has at least the number of points
3353 # of minimum_points_num, which is a bit more than the face-loops limit.
3354 # If not, subdivide to reach at least that number of points
3355 for i in range(len(self.temporary_curve.data.splines)):
3356 sp = self.temporary_curve.data.splines[i]
3358 if len(sp.bezier_points) < minimum_points_num:
3359 for bp in sp.bezier_points:
3360 bp.select_control_point = True
3362 if (len(sp.bezier_points) - 1) != 0:
3363 # Formula to get the number of cuts that will make a curve
3364 # of N number of points have near to "minimum_points_num"
3365 # points, when subdividing with this number of cuts
3366 subdivide_cuts = int(
3367 (minimum_points_num - len(sp.bezier_points)) /
3368 (len(sp.bezier_points) - 1)
3369 ) + 1
3370 else:
3371 subdivide_cuts = 0
3373 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3374 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3376 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3378 # Detect if the strokes are a crosshatch and do it if it is
3379 self.crosshatch_surface_invoke(self.temporary_curve)
3381 if not self.is_crosshatch:
3382 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3383 self.temporary_curve.select_set(True)
3384 bpy.context.view_layer.objects.active = self.temporary_curve
3386 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3388 # Set a minimum number of points for rectangular surfaces
3389 minimum_points_num = 60
3391 # Check if the number of points of each curve has at least the number of points
3392 # of minimum_points_num, which is a bit more than the face-loops limit.
3393 # If not, subdivide to reach at least that number of points
3394 for i in range(len(self.temporary_curve.data.splines)):
3395 sp = self.temporary_curve.data.splines[i]
3397 if len(sp.bezier_points) < minimum_points_num:
3398 for bp in sp.bezier_points:
3399 bp.select_control_point = True
3401 if (len(sp.bezier_points) - 1) != 0:
3402 # Formula to get the number of cuts that will make a curve of
3403 # N number of points have near to "minimum_points_num" points,
3404 # when subdividing with this number of cuts
3405 subdivide_cuts = int(
3406 (minimum_points_num - len(sp.bezier_points)) /
3407 (len(sp.bezier_points) - 1)
3408 ) + 1
3409 else:
3410 subdivide_cuts = 0
3412 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
3413 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
3415 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3417 # Save coordinates of the actual strokes (as the "last saved splines")
3418 for sp_idx in range(len(self.temporary_curve.data.splines)):
3419 self.last_strokes_splines_coords.append([])
3420 for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
3421 coords = self.temporary_curve.matrix_world @ \
3422 self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
3423 self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
3425 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3426 for sp_idx in range(len(self.temporary_curve.data.splines)):
3427 if self.temporary_curve.data.splines[sp_idx].use_cyclic_u is True:
3428 first_p_co = self.last_strokes_splines_coords[sp_idx][0]
3429 last_p_co = self.last_strokes_splines_coords[sp_idx][
3430 len(self.last_strokes_splines_coords[sp_idx]) - 1
3432 target_co = [
3433 (first_p_co[0] + last_p_co[0]) / 2,
3434 (first_p_co[1] + last_p_co[1]) / 2,
3435 (first_p_co[2] + last_p_co[2]) / 2
3438 self.last_strokes_splines_coords[sp_idx][0] = target_co
3439 self.last_strokes_splines_coords[sp_idx][
3440 len(self.last_strokes_splines_coords[sp_idx]) - 1
3441 ] = target_co
3442 tuple(self.last_strokes_splines_coords)
3444 # Estimation of the average length of the segments between
3445 # each point of the grease pencil strokes.
3446 # Will be useful to determine whether a curve should be made "Cyclic"
3447 segments_lengths_sum = 0
3448 segments_count = 0
3449 random_spline = self.temporary_curve.data.splines[0].bezier_points
3450 for i in range(0, len(random_spline)):
3451 if i != 0 and len(random_spline) - 1 >= i:
3452 segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
3453 segments_count += 1
3455 self.average_gp_segment_length = segments_lengths_sum / segments_count
3457 # Delete temporary strokes curve object
3458 bpy.ops.object.delete({"selected_objects": [self.temporary_curve]})
3460 # Set again since "execute()" will turn it again to its initial value
3461 self.execute(context)
3463 if not self.stopping_errors:
3464 # Delete grease pencil strokes
3465 if self.strokes_type == "GP_STROKES":
3466 try:
3467 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3468 except:
3469 pass
3471 # Delete annotation strokes
3472 elif self.strokes_type == "GP_ANNOTATION":
3473 try:
3474 bpy.context.annotation_data.layers.active.clear()
3475 except:
3476 pass
3478 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3479 bpy.ops.object.delete({"selected_objects": [self.original_curve]})
3480 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3482 return {"FINISHED"}
3483 else:
3484 return{"CANCELLED"}
3486 elif self.strokes_type == "SELECTION_ALONE":
3487 self.is_fill_faces = True
3488 created_faces_count = self.fill_with_faces(self.main_object)
3490 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3492 if created_faces_count == 0:
3493 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3494 return {"CANCELLED"}
3495 else:
3496 return {"FINISHED"}
3498 if self.strokes_type == "EXTERNAL_NO_CURVE":
3499 self.report({'WARNING'}, "The secondary object is not a Curve.")
3500 return{"CANCELLED"}
3502 elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
3503 self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3504 return{"CANCELLED"}
3506 elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or \
3507 self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
3509 self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3510 return{"CANCELLED"}
3512 elif self.strokes_type == "NO_STROKES":
3513 self.report({'WARNING'}, "There aren't any strokes attached to the object")
3514 return{"CANCELLED"}
3516 elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
3517 self.report({'WARNING'}, "All splines must be Bezier.")
3518 return{"CANCELLED"}
3520 else:
3521 return{"CANCELLED"}
3523 # ----------------------------
3524 # Init operator
3525 class MESH_OT_SURFSK_init(Operator):
3526 bl_idname = "mesh.surfsk_init"
3527 bl_label = "Bsurfaces initialize"
3528 bl_description = "Add an empty mesh object with useful settings"
3529 bl_options = {'REGISTER', 'UNDO'}
3531 def execute(self, context):
3533 bs = bpy.context.scene.bsurfaces
3535 if bpy.ops.object.mode_set.poll():
3536 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3538 global global_color
3539 global global_offset
3540 global global_in_front
3541 global global_show_wire
3542 global global_shade_smooth
3543 global global_mesh_object
3544 global global_gpencil_object
3546 if bs.SURFSK_mesh == None:
3547 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3548 mesh = bpy.data.meshes.new('BSurfaceMesh')
3549 mesh_object = object_utils.object_data_add(context, mesh)
3550 mesh_object.select_set(True)
3551 bpy.context.view_layer.objects.active = mesh_object
3553 mesh_object.show_all_edges = True
3554 global_in_front = bpy.context.scene.bsurfaces.SURFSK_in_front
3555 mesh_object.show_in_front = global_in_front
3556 mesh_object.display_type = 'SOLID'
3557 mesh_object.show_wire = True
3559 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
3560 if global_shade_smooth:
3561 bpy.ops.object.shade_smooth()
3562 else:
3563 bpy.ops.object.shade_flat()
3565 global_show_wire = bpy.context.scene.bsurfaces.SURFSK_show_wire
3566 mesh_object.show_wire = global_show_wire
3568 global_color = bpy.context.scene.bsurfaces.SURFSK_mesh_color
3569 material = makeMaterial("BSurfaceMesh", global_color)
3570 mesh_object.data.materials.append(material)
3571 bpy.ops.object.modifier_add(type='SHRINKWRAP')
3572 modifier = mesh_object.modifiers["Shrinkwrap"]
3573 if self.active_object is not None:
3574 modifier.target = self.active_object
3575 modifier.wrap_method = 'TARGET_PROJECT'
3576 modifier.wrap_mode = 'OUTSIDE_SURFACE'
3577 modifier.show_on_cage = True
3578 global_offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3579 modifier.offset = global_offset
3581 global_mesh_object = mesh_object.name
3582 bpy.context.scene.bsurfaces.SURFSK_mesh = bpy.data.objects[global_mesh_object]
3584 bpy.context.scene.tool_settings.snap_elements = {'FACE'}
3585 bpy.context.scene.tool_settings.use_snap = True
3586 bpy.context.scene.tool_settings.use_snap_self = False
3587 bpy.context.scene.tool_settings.use_snap_align_rotation = True
3588 bpy.context.scene.tool_settings.use_snap_project = True
3589 bpy.context.scene.tool_settings.use_snap_rotate = True
3590 bpy.context.scene.tool_settings.use_snap_scale = True
3592 bpy.context.scene.tool_settings.use_mesh_automerge = True
3593 bpy.context.scene.tool_settings.double_threshold = 0.01
3595 if context.scene.bsurfaces.SURFSK_guide == 'GPencil' and bs.SURFSK_gpencil == None:
3596 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3597 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')
3598 bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d = 'SURFACE'
3599 gpencil_object = bpy.context.scene.objects[bpy.context.scene.objects[-1].name]
3600 gpencil_object.select_set(True)
3601 bpy.context.view_layer.objects.active = gpencil_object
3602 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3603 global_gpencil_object = gpencil_object.name
3604 bpy.context.scene.bsurfaces.SURFSK_gpencil = bpy.data.objects[global_gpencil_object]
3605 gpencil_object.data.stroke_depth_order = '3D'
3606 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3607 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3609 if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
3610 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3611 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3613 def invoke(self, context, event):
3614 if bpy.context.active_object:
3615 self.active_object = bpy.context.active_object
3616 else:
3617 self.active_object = None
3619 self.execute(context)
3621 return {"FINISHED"}
3623 # ----------------------------
3624 # Add modifiers operator
3625 class MESH_OT_SURFSK_add_modifiers(Operator):
3626 bl_idname = "mesh.surfsk_add_modifiers"
3627 bl_label = "Add Mirror and others modifiers"
3628 bl_description = "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
3629 bl_options = {'REGISTER', 'UNDO'}
3631 def execute(self, context):
3633 bs = bpy.context.scene.bsurfaces
3635 if bpy.ops.object.mode_set.poll():
3636 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3638 if bs.SURFSK_mesh == None:
3639 self.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
3640 else:
3641 mesh_object = bs.SURFSK_mesh
3643 try:
3644 mesh_object.select_set(True)
3645 except:
3646 self.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
3647 return {"CANCEL"}
3649 bpy.context.view_layer.objects.active = mesh_object
3651 try:
3652 shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
3653 if self.active_object is not None and self.active_object != mesh_object:
3654 shrinkwrap.target = self.active_object
3655 shrinkwrap.wrap_method = 'TARGET_PROJECT'
3656 shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
3657 shrinkwrap.show_on_cage = True
3658 shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3659 except:
3660 bpy.ops.object.modifier_add(type='SHRINKWRAP')
3661 shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
3662 if self.active_object is not None and self.active_object != mesh_object:
3663 shrinkwrap.target = self.active_object
3664 shrinkwrap.wrap_method = 'TARGET_PROJECT'
3665 shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
3666 shrinkwrap.show_on_cage = True
3667 shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
3669 try:
3670 mirror = mesh_object.modifiers["Mirror"]
3671 mirror.use_clip = True
3672 except:
3673 bpy.ops.object.modifier_add(type='MIRROR')
3674 mirror = mesh_object.modifiers["Mirror"]
3675 mirror.use_clip = True
3677 try:
3678 _subsurf = mesh_object.modifiers["Subdivision"]
3679 except:
3680 bpy.ops.object.modifier_add(type='SUBSURF')
3681 _subsurf = mesh_object.modifiers["Subdivision"]
3683 try:
3684 solidify = mesh_object.modifiers["Solidify"]
3685 solidify.thickness = 0.01
3686 except:
3687 bpy.ops.object.modifier_add(type='SOLIDIFY')
3688 solidify = mesh_object.modifiers["Solidify"]
3689 solidify.thickness = 0.01
3691 return {"FINISHED"}
3693 def invoke(self, context, event):
3694 if bpy.context.active_object:
3695 self.active_object = bpy.context.active_object
3696 else:
3697 self.active_object = None
3699 self.execute(context)
3701 return {"FINISHED"}
3703 # ----------------------------
3704 # Edit surface operator
3705 class MESH_OT_SURFSK_edit_surface(Operator):
3706 bl_idname = "mesh.surfsk_edit_surface"
3707 bl_label = "Bsurfaces edit surface"
3708 bl_description = "Edit surface mesh"
3709 bl_options = {'REGISTER', 'UNDO'}
3711 def execute(self, context):
3712 if bpy.ops.object.mode_set.poll():
3713 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3714 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3715 bpy.context.scene.bsurfaces.SURFSK_mesh.select_set(True)
3716 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_mesh
3717 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3718 bpy.ops.wm.tool_set_by_id(name="builtin.select")
3720 def invoke(self, context, event):
3721 try:
3722 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
3723 bpy.data.objects[global_mesh_object].select_set(True)
3724 self.main_object = bpy.data.objects[global_mesh_object]
3725 bpy.context.view_layer.objects.active = self.main_object
3726 except:
3727 self.report({'WARNING'}, "Specify the name of the object with retopology")
3728 return{"CANCELLED"}
3730 self.execute(context)
3732 return {"FINISHED"}
3734 # ----------------------------
3735 # Add strokes operator
3736 class GPENCIL_OT_SURFSK_add_strokes(Operator):
3737 bl_idname = "gpencil.surfsk_add_strokes"
3738 bl_label = "Bsurfaces add strokes"
3739 bl_description = "Add the grease pencil strokes"
3740 bl_options = {'REGISTER', 'UNDO'}
3742 def execute(self, context):
3743 if bpy.ops.object.mode_set.poll():
3744 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3745 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3747 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3748 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_gpencil
3749 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
3750 bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
3752 return{"FINISHED"}
3754 def invoke(self, context, event):
3755 try:
3756 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3757 except:
3758 self.report({'WARNING'}, "Specify the name of the object with strokes")
3759 return{"CANCELLED"}
3761 self.execute(context)
3763 return {"FINISHED"}
3765 # ----------------------------
3766 # Edit strokes operator
3767 class GPENCIL_OT_SURFSK_edit_strokes(Operator):
3768 bl_idname = "gpencil.surfsk_edit_strokes"
3769 bl_label = "Bsurfaces edit strokes"
3770 bl_description = "Edit the grease pencil strokes"
3771 bl_options = {'REGISTER', 'UNDO'}
3773 def execute(self, context):
3774 if bpy.ops.object.mode_set.poll():
3775 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3776 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3778 gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil
3780 gpencil_object.select_set(True)
3781 bpy.context.view_layer.objects.active = gpencil_object
3783 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT_GPENCIL')
3784 try:
3785 bpy.ops.gpencil.select_all(action='SELECT')
3786 except:
3787 pass
3789 def invoke(self, context, event):
3790 try:
3791 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3792 except:
3793 self.report({'WARNING'}, "Specify the name of the object with strokes")
3794 return{"CANCELLED"}
3796 self.execute(context)
3798 return {"FINISHED"}
3800 # ----------------------------
3801 # Convert annotation to curves operator
3802 class GPENCIL_OT_SURFSK_annotation_to_curves(Operator):
3803 bl_idname = "gpencil.surfsk_annotations_to_curves"
3804 bl_label = "Convert annotation to curves"
3805 bl_description = "Convert annotation to curves for editing"
3806 bl_options = {'REGISTER', 'UNDO'}
3808 def execute(self, context):
3810 if bpy.ops.object.mode_set.poll():
3811 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3813 # Convert annotation to curve
3814 curve = conver_gpencil_to_curve(self, context, None, 'Annotation')
3816 if curve != None:
3817 # Delete annotation strokes
3818 try:
3819 bpy.context.annotation_data.layers.active.clear()
3820 except:
3821 pass
3823 # Clean up curves
3824 curve.select_set(True)
3825 bpy.context.view_layer.objects.active = curve
3827 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3829 return {"FINISHED"}
3831 def invoke(self, context, event):
3832 try:
3833 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
3835 _strokes_num = len(strokes)
3836 except:
3837 self.report({'WARNING'}, "Not active annotation")
3838 return{"CANCELLED"}
3840 self.execute(context)
3842 return {"FINISHED"}
3844 # ----------------------------
3845 # Convert strokes to curves operator
3846 class GPENCIL_OT_SURFSK_strokes_to_curves(Operator):
3847 bl_idname = "gpencil.surfsk_strokes_to_curves"
3848 bl_label = "Convert strokes to curves"
3849 bl_description = "Convert grease pencil strokes to curves for editing"
3850 bl_options = {'REGISTER', 'UNDO'}
3852 def execute(self, context):
3854 if bpy.ops.object.mode_set.poll():
3855 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3857 # Convert grease pencil strokes to curve
3858 gp = bpy.context.scene.bsurfaces.SURFSK_gpencil
3859 curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
3861 if curve != None:
3862 # Delete grease pencil strokes
3863 try:
3864 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
3865 except:
3866 pass
3868 # Clean up curves
3870 curve.select_set(True)
3871 bpy.context.view_layer.objects.active = curve
3873 bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
3875 return {"FINISHED"}
3877 def invoke(self, context, event):
3878 try:
3879 bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
3880 except:
3881 self.report({'WARNING'}, "Specify the name of the object with strokes")
3882 return{"CANCELLED"}
3884 self.execute(context)
3886 return {"FINISHED"}
3888 # ----------------------------
3889 # Add annotation
3890 class GPENCIL_OT_SURFSK_add_annotation(Operator):
3891 bl_idname = "gpencil.surfsk_add_annotation"
3892 bl_label = "Bsurfaces add annotation"
3893 bl_description = "Add annotation"
3894 bl_options = {'REGISTER', 'UNDO'}
3896 def execute(self, context):
3897 bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
3898 bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
3900 return{"FINISHED"}
3902 def invoke(self, context, event):
3904 self.execute(context)
3906 return {"FINISHED"}
3909 # ----------------------------
3910 # Edit curve operator
3911 class CURVE_OT_SURFSK_edit_curve(Operator):
3912 bl_idname = "curve.surfsk_edit_curve"
3913 bl_label = "Bsurfaces edit curve"
3914 bl_description = "Edit curve"
3915 bl_options = {'REGISTER', 'UNDO'}
3917 def execute(self, context):
3918 if bpy.ops.object.mode_set.poll():
3919 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
3920 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3921 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3922 bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_curve
3923 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
3925 def invoke(self, context, event):
3926 try:
3927 bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
3928 except:
3929 self.report({'WARNING'}, "Specify the name of the object with curve")
3930 return{"CANCELLED"}
3932 self.execute(context)
3934 return {"FINISHED"}
3936 # ----------------------------
3937 # Reorder splines
3938 class CURVE_OT_SURFSK_reorder_splines(Operator):
3939 bl_idname = "curve.surfsk_reorder_splines"
3940 bl_label = "Bsurfaces reorder splines"
3941 bl_description = "Defines the order of the splines by using grease pencil strokes"
3942 bl_options = {'REGISTER', 'UNDO'}
3944 def execute(self, context):
3945 objects_to_delete = []
3946 # Convert grease pencil strokes to curve.
3947 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3948 bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3949 for ob in bpy.context.selected_objects:
3950 if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
3951 GP_strokes_curve = ob
3953 # GP_strokes_curve = bpy.context.object
3954 objects_to_delete.append(GP_strokes_curve)
3956 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3957 GP_strokes_curve.select_set(True)
3958 bpy.context.view_layer.objects.active = GP_strokes_curve
3960 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3961 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
3962 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=100)
3963 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3965 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3966 GP_strokes_mesh = bpy.context.object
3967 objects_to_delete.append(GP_strokes_mesh)
3969 GP_strokes_mesh.data.resolution_u = 1
3970 bpy.ops.object.convert(target='MESH', keep_original=False)
3972 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3973 self.main_curve.select_set(True)
3974 bpy.context.view_layer.objects.active = self.main_curve
3976 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
3977 curves_duplicate_1 = bpy.context.object
3978 objects_to_delete.append(curves_duplicate_1)
3980 minimum_points_num = 500
3982 # Some iterations since the subdivision operator
3983 # has a limit of 100 subdivisions per iteration
3984 for x in range(round(minimum_points_num / 100)):
3985 # Check if the number of points of each curve has at least the number of points
3986 # of minimum_points_num. If not, subdivide to reach at least that number of points
3987 for i in range(len(curves_duplicate_1.data.splines)):
3988 sp = curves_duplicate_1.data.splines[i]
3990 if len(sp.bezier_points) < minimum_points_num:
3991 for bp in sp.bezier_points:
3992 bp.select_control_point = True
3994 if (len(sp.bezier_points) - 1) != 0:
3995 # Formula to get the number of cuts that will make a curve of N
3996 # number of points have near to "minimum_points_num" points,
3997 # when subdividing with this number of cuts
3998 subdivide_cuts = int(
3999 (minimum_points_num - len(sp.bezier_points)) /
4000 (len(sp.bezier_points) - 1)
4001 ) + 1
4002 else:
4003 subdivide_cuts = 0
4005 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4006 bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
4007 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4008 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4010 bpy.ops.object.duplicate('INVOKE_REGION_WIN')
4011 curves_duplicate_2 = bpy.context.object
4012 objects_to_delete.append(curves_duplicate_2)
4014 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
4015 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4016 curves_duplicate_2.select_set(True)
4017 bpy.context.view_layer.objects.active = curves_duplicate_2
4019 bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
4020 curves_duplicate_2.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX"
4021 curves_duplicate_2.modifiers["Shrinkwrap"].target = GP_strokes_mesh
4022 bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier='Shrinkwrap')
4024 # Get the distance of each vert from its original position to its position with Shrinkwrap
4025 nearest_points_coords = {}
4026 for st_idx in range(len(curves_duplicate_1.data.splines)):
4027 for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
4028 bp_1_co = curves_duplicate_1.matrix_world @ \
4029 curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
4031 bp_2_co = curves_duplicate_2.matrix_world @ \
4032 curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
4034 if bp_idx == 0:
4035 shortest_dist = (bp_1_co - bp_2_co).length
4036 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
4037 "%.4f" % bp_2_co[1],
4038 "%.4f" % bp_2_co[2])
4040 dist = (bp_1_co - bp_2_co).length
4042 if dist < shortest_dist:
4043 nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
4044 "%.4f" % bp_2_co[1],
4045 "%.4f" % bp_2_co[2])
4046 shortest_dist = dist
4048 # Get all coords of GP strokes points, for comparison
4049 GP_strokes_coords = []
4050 for st_idx in range(len(GP_strokes_curve.data.splines)):
4051 GP_strokes_coords.append(
4052 [("%.4f" % x if "%.4f" % x != "-0.00" else "0.00",
4053 "%.4f" % y if "%.4f" % y != "-0.00" else "0.00",
4054 "%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for
4055 x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]]
4058 # Check the point of the GP strokes with the same coords as
4059 # the nearest points of the curves (with shrinkwrap)
4061 # Dictionary with GP stroke index as index, and a list as value.
4062 # The list has as index the point index of the GP stroke
4063 # nearest to the spline, and as value the spline index
4064 GP_connection_points = {}
4065 for gp_st_idx in range(len(GP_strokes_coords)):
4066 GPvert_spline_relationship = {}
4068 for splines_st_idx in range(len(nearest_points_coords)):
4069 if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
4070 GPvert_spline_relationship[
4071 GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])
4072 ] = splines_st_idx
4074 GP_connection_points[gp_st_idx] = GPvert_spline_relationship
4076 # Get the splines new order
4077 splines_new_order = []
4078 for i in GP_connection_points:
4079 dict_keys = sorted(GP_connection_points[i].keys()) # Sort dictionaries by key
4081 for k in dict_keys:
4082 splines_new_order.append(GP_connection_points[i][k])
4084 # Reorder
4085 curve_original_name = self.main_curve.name
4087 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4088 self.main_curve.select_set(True)
4089 bpy.context.view_layer.objects.active = self.main_curve
4091 self.main_curve.name = "SURFSKIO_CRV_ORD"
4093 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4094 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4095 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4097 for _sp_idx in range(len(self.main_curve.data.splines)):
4098 self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
4100 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4101 bpy.ops.curve.separate('EXEC_REGION_WIN')
4102 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4104 # Get the names of the separated splines objects in the original order
4105 splines_unordered = {}
4106 for o in bpy.data.objects:
4107 if o.name.find("SURFSKIO_CRV_ORD") != -1:
4108 spline_order_string = o.name.partition(".")[2]
4110 if spline_order_string != "" and int(spline_order_string) > 0:
4111 spline_order_index = int(spline_order_string) - 1
4112 splines_unordered[spline_order_index] = o.name
4114 # Join all splines objects in final order
4115 for order_idx in splines_new_order:
4116 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4117 bpy.data.objects[splines_unordered[order_idx]].select_set(True)
4118 bpy.data.objects["SURFSKIO_CRV_ORD"].select_set(True)
4119 bpy.context.view_layer.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
4121 bpy.ops.object.join('INVOKE_REGION_WIN')
4123 # Go back to the original name of the curves object.
4124 bpy.context.object.name = curve_original_name
4126 # Delete all unused objects
4127 bpy.ops.object.delete({"selected_objects": objects_to_delete})
4129 bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
4130 bpy.data.objects[curve_original_name].select_set(True)
4131 bpy.context.view_layer.objects.active = bpy.data.objects[curve_original_name]
4133 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4134 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4136 try:
4137 bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
4138 except:
4139 pass
4142 return {"FINISHED"}
4144 def invoke(self, context, event):
4145 self.main_curve = bpy.context.object
4146 there_are_GP_strokes = False
4148 try:
4149 # Get the active grease pencil layer
4150 strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
4152 if strokes_num > 0:
4153 there_are_GP_strokes = True
4154 except:
4155 pass
4157 if there_are_GP_strokes:
4158 self.execute(context)
4159 self.report({'INFO'}, "Splines have been reordered")
4160 else:
4161 self.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
4163 return {"FINISHED"}
4165 # ----------------------------
4166 # Set first points operator
4167 class CURVE_OT_SURFSK_first_points(Operator):
4168 bl_idname = "curve.surfsk_first_points"
4169 bl_label = "Bsurfaces set first points"
4170 bl_description = "Set the selected points as the first point of each spline"
4171 bl_options = {'REGISTER', 'UNDO'}
4173 def execute(self, context):
4174 splines_to_invert = []
4176 # Check non-cyclic splines to invert
4177 for i in range(len(self.main_curve.data.splines)):
4178 b_points = self.main_curve.data.splines[i].bezier_points
4180 if i not in self.cyclic_splines: # Only for non-cyclic splines
4181 if b_points[len(b_points) - 1].select_control_point:
4182 splines_to_invert.append(i)
4184 # Reorder points of cyclic splines, and set all handles to "Automatic"
4186 # Check first selected point
4187 cyclic_splines_new_first_pt = {}
4188 for i in self.cyclic_splines:
4189 sp = self.main_curve.data.splines[i]
4191 for t in range(len(sp.bezier_points)):
4192 bp = sp.bezier_points[t]
4193 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
4194 cyclic_splines_new_first_pt[i] = t
4195 break # To take only one if there are more
4197 # Reorder
4198 for spline_idx in cyclic_splines_new_first_pt:
4199 sp = self.main_curve.data.splines[spline_idx]
4201 spline_old_coords = []
4202 for bp_old in sp.bezier_points:
4203 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
4205 left_handle_type = str(bp_old.handle_left_type)
4206 left_handle_length = float(bp_old.handle_left.length)
4207 left_handle_xyz = (
4208 float(bp_old.handle_left.x),
4209 float(bp_old.handle_left.y),
4210 float(bp_old.handle_left.z)
4212 right_handle_type = str(bp_old.handle_right_type)
4213 right_handle_length = float(bp_old.handle_right.length)
4214 right_handle_xyz = (
4215 float(bp_old.handle_right.x),
4216 float(bp_old.handle_right.y),
4217 float(bp_old.handle_right.z)
4219 spline_old_coords.append(
4220 [coords, left_handle_type,
4221 right_handle_type, left_handle_length,
4222 right_handle_length, left_handle_xyz,
4223 right_handle_xyz]
4226 for t in range(len(sp.bezier_points)):
4227 bp = sp.bezier_points
4229 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
4230 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
4231 else:
4232 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
4234 bp[t].co = Vector(spline_old_coords[new_index][0])
4236 bp[t].handle_left.length = spline_old_coords[new_index][3]
4237 bp[t].handle_right.length = spline_old_coords[new_index][4]
4239 bp[t].handle_left_type = "FREE"
4240 bp[t].handle_right_type = "FREE"
4242 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
4243 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
4244 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
4246 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
4247 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
4248 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
4250 bp[t].handle_left_type = spline_old_coords[new_index][1]
4251 bp[t].handle_right_type = spline_old_coords[new_index][2]
4253 # Invert the non-cyclic splines designated above
4254 for i in range(len(splines_to_invert)):
4255 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4257 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4258 self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
4259 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4261 bpy.ops.curve.switch_direction()
4263 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
4265 # Keep selected the first vert of each spline
4266 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4267 for i in range(len(self.main_curve.data.splines)):
4268 if not self.main_curve.data.splines[i].use_cyclic_u:
4269 bp = self.main_curve.data.splines[i].bezier_points[0]
4270 else:
4271 bp = self.main_curve.data.splines[i].bezier_points[
4272 len(self.main_curve.data.splines[i].bezier_points) - 1
4275 bp.select_control_point = True
4276 bp.select_right_handle = True
4277 bp.select_left_handle = True
4279 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4281 return {'FINISHED'}
4283 def invoke(self, context, event):
4284 self.main_curve = bpy.context.object
4286 # Check if all curves are Bezier, and detect which ones are cyclic
4287 self.cyclic_splines = []
4288 for i in range(len(self.main_curve.data.splines)):
4289 if self.main_curve.data.splines[i].type != "BEZIER":
4290 self.report({'WARNING'}, "All splines must be Bezier type")
4292 return {'CANCELLED'}
4293 else:
4294 if self.main_curve.data.splines[i].use_cyclic_u:
4295 self.cyclic_splines.append(i)
4297 self.execute(context)
4298 self.report({'INFO'}, "First points have been set")
4300 return {'FINISHED'}
4303 # Add-ons Preferences Update Panel
4305 # Define Panel classes for updating
4306 panels = (
4307 VIEW3D_PT_tools_SURFSK_mesh,
4308 VIEW3D_PT_tools_SURFSK_curve
4312 def conver_gpencil_to_curve(self, context, pencil, type):
4313 newCurve = bpy.data.curves.new(type + '_curve', type='CURVE')
4314 newCurve.dimensions = '3D'
4315 CurveObject = object_utils.object_data_add(context, newCurve)
4316 error = False
4318 if type == 'GPensil':
4319 try:
4320 strokes = pencil.data.layers.active.active_frame.strokes
4321 except:
4322 error = True
4323 CurveObject.location = pencil.location
4324 CurveObject.rotation_euler = pencil.rotation_euler
4325 CurveObject.scale = pencil.scale
4326 elif type == 'Annotation':
4327 try:
4328 strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
4329 except:
4330 error = True
4331 CurveObject.location = (0.0, 0.0, 0.0)
4332 CurveObject.rotation_euler = (0.0, 0.0, 0.0)
4333 CurveObject.scale = (1.0, 1.0, 1.0)
4335 if not error:
4336 for i, _stroke in enumerate(strokes):
4337 stroke_points = strokes[i].points
4338 data_list = [ (point.co.x, point.co.y, point.co.z)
4339 for point in stroke_points ]
4340 points_to_add = len(data_list)-1
4342 flat_list = []
4343 for point in data_list:
4344 flat_list.extend(point)
4346 spline = newCurve.splines.new(type='BEZIER')
4347 spline.bezier_points.add(points_to_add)
4348 spline.bezier_points.foreach_set("co", flat_list)
4350 for point in spline.bezier_points:
4351 point.handle_left_type="AUTO"
4352 point.handle_right_type="AUTO"
4354 return CurveObject
4355 else:
4356 return None
4359 def update_panel(self, context):
4360 message = "Bsurfaces GPL Edition: Updating Panel locations has failed"
4361 try:
4362 for panel in panels:
4363 if "bl_rna" in panel.__dict__:
4364 bpy.utils.unregister_class(panel)
4366 for panel in panels:
4367 category = context.preferences.addons[__name__].preferences.category
4368 if category != 'Tool':
4369 panel.bl_category = context.preferences.addons[__name__].preferences.category
4370 else:
4371 context.preferences.addons[__name__].preferences.category = 'Edit'
4372 panel.bl_category = 'Edit'
4373 raise ValueError("You can not install add-ons in the Tool panel")
4374 bpy.utils.register_class(panel)
4376 except Exception as e:
4377 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
4378 pass
4380 def makeMaterial(name, diffuse):
4382 if name in bpy.data.materials:
4383 material = bpy.data.materials[name]
4384 material.diffuse_color = diffuse
4385 else:
4386 material = bpy.data.materials.new(name)
4387 material.diffuse_color = diffuse
4389 return material
4391 def update_mesh(self, context):
4392 try:
4393 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4394 bpy.ops.object.select_all(action='DESELECT')
4395 bpy.context.view_layer.update()
4396 global global_mesh_object
4397 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4398 bpy.data.objects[global_mesh_object].select_set(True)
4399 bpy.context.view_layer.objects.active = bpy.data.objects[global_mesh_object]
4400 except:
4401 print("Select mesh object")
4403 def update_gpencil(self, context):
4404 try:
4405 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4406 bpy.ops.object.select_all(action='DESELECT')
4407 bpy.context.view_layer.update()
4408 global global_gpencil_object
4409 global_gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil.name
4410 bpy.data.objects[global_gpencil_object].select_set(True)
4411 bpy.context.view_layer.objects.active = bpy.data.objects[global_gpencil_object]
4412 except:
4413 print("Select gpencil object")
4415 def update_curve(self, context):
4416 try:
4417 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4418 bpy.ops.object.select_all(action='DESELECT')
4419 bpy.context.view_layer.update()
4420 global global_curve_object
4421 global_curve_object = bpy.context.scene.bsurfaces.SURFSK_curve.name
4422 bpy.data.objects[global_curve_object].select_set(True)
4423 bpy.context.view_layer.objects.active = bpy.data.objects[global_curve_object]
4424 except:
4425 print("Select curve object")
4427 def update_color(self, context):
4428 try:
4429 global global_color
4430 global global_mesh_object
4431 material = makeMaterial("BSurfaceMesh", bpy.context.scene.bsurfaces.SURFSK_mesh_color)
4432 if bpy.data.objects[global_mesh_object].data.materials:
4433 bpy.data.objects[global_mesh_object].data.materials[0] = material
4434 else:
4435 bpy.data.objects[global_mesh_object].data.materials.append(material)
4436 diffuse_color = material.diffuse_color
4437 global_color = (diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3])
4438 except:
4439 print("Select mesh object")
4441 def update_Shrinkwrap_offset(self, context):
4442 try:
4443 global global_offset
4444 global_offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
4445 global global_mesh_object
4446 modifier = bpy.data.objects[global_mesh_object].modifiers["Shrinkwrap"]
4447 modifier.offset = global_offset
4448 except:
4449 print("Shrinkwrap modifier not found")
4451 def update_in_front(self, context):
4452 try:
4453 global global_in_front
4454 global_in_front = bpy.context.scene.bsurfaces.SURFSK_in_front
4455 global global_mesh_object
4456 bpy.data.objects[global_mesh_object].show_in_front = global_in_front
4457 except:
4458 print("Select mesh object")
4460 def update_show_wire(self, context):
4461 try:
4462 global global_show_wire
4463 global_show_wire = bpy.context.scene.bsurfaces.SURFSK_show_wire
4464 global global_mesh_object
4465 bpy.data.objects[global_mesh_object].show_wire = global_show_wire
4466 except:
4467 print("Select mesh object")
4469 def update_shade_smooth(self, context):
4470 try:
4471 global global_shade_smooth
4472 global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
4474 contex_mode = bpy.context.mode
4476 if bpy.ops.object.mode_set.poll():
4477 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
4479 bpy.ops.object.select_all(action='DESELECT')
4480 global global_mesh_object
4481 global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
4482 bpy.data.objects[global_mesh_object].select_set(True)
4484 if global_shade_smooth:
4485 bpy.ops.object.shade_smooth()
4486 else:
4487 bpy.ops.object.shade_flat()
4489 if contex_mode == "EDIT_MESH":
4490 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
4492 except:
4493 print("Select mesh object")
4496 class BsurfPreferences(AddonPreferences):
4497 # this must match the addon name, use '__package__'
4498 # when defining this in a submodule of a python package.
4499 bl_idname = __name__
4501 category: StringProperty(
4502 name="Tab Category",
4503 description="Choose a name for the category of the panel",
4504 default="Edit",
4505 update=update_panel
4508 def draw(self, context):
4509 layout = self.layout
4511 row = layout.row()
4512 col = row.column()
4513 col.label(text="Tab Category:")
4514 col.prop(self, "category", text="")
4516 # Properties
4517 class BsurfacesProps(PropertyGroup):
4518 SURFSK_guide: EnumProperty(
4519 name="Guide:",
4520 items=[
4521 ('Annotation', 'Annotation', 'Annotation'),
4522 ('GPencil', 'GPencil', 'GPencil'),
4523 ('Curve', 'Curve', 'Curve')
4525 default="Annotation"
4527 SURFSK_edges_U: IntProperty(
4528 name="Cross",
4529 description="Number of face-loops crossing the strokes",
4530 default=5,
4531 min=1,
4532 max=200
4534 SURFSK_edges_V: IntProperty(
4535 name="Follow",
4536 description="Number of face-loops following the strokes",
4537 default=1,
4538 min=1,
4539 max=200
4541 SURFSK_cyclic_cross: BoolProperty(
4542 name="Cyclic Cross",
4543 description="Make cyclic the face-loops crossing the strokes",
4544 default=False
4546 SURFSK_cyclic_follow: BoolProperty(
4547 name="Cyclic Follow",
4548 description="Make cyclic the face-loops following the strokes",
4549 default=False
4551 SURFSK_keep_strokes: BoolProperty(
4552 name="Keep strokes",
4553 description="Keeps the sketched strokes or curves after adding the surface",
4554 default=False
4556 SURFSK_automatic_join: BoolProperty(
4557 name="Automatic join",
4558 description="Join automatically vertices of either surfaces "
4559 "generated by crosshatching, or from the borders of closed shapes",
4560 default=True
4562 SURFSK_loops_on_strokes: BoolProperty(
4563 name="Loops on strokes",
4564 description="Make the loops match the paths of the strokes",
4565 default=True
4567 SURFSK_precision: IntProperty(
4568 name="Precision",
4569 description="Precision level of the surface calculation",
4570 default=2,
4571 min=1,
4572 max=100
4574 SURFSK_mesh: PointerProperty(
4575 name="Mesh of BSurface",
4576 type=bpy.types.Object,
4577 description="Mesh of BSurface",
4578 update=update_mesh,
4580 SURFSK_gpencil: PointerProperty(
4581 name="GreasePencil object",
4582 type=bpy.types.Object,
4583 description="GreasePencil object",
4584 update=update_gpencil,
4586 SURFSK_curve: PointerProperty(
4587 name="Curve object",
4588 type=bpy.types.Object,
4589 description="Curve object",
4590 update=update_curve,
4592 SURFSK_mesh_color: FloatVectorProperty(
4593 name="Mesh color",
4594 default=(1.0, 0.0, 0.0, 0.3),
4595 size=4,
4596 subtype="COLOR",
4597 min=0,
4598 max=1,
4599 update=update_color,
4600 description="Mesh color",
4602 SURFSK_Shrinkwrap_offset: FloatProperty(
4603 name="Shrinkwrap offset",
4604 default=0.01,
4605 precision=3,
4606 description="Distance to keep from the target",
4607 update=update_Shrinkwrap_offset,
4609 SURFSK_in_front: BoolProperty(
4610 name="In Front",
4611 description="Make the object draw in front of others",
4612 default=False,
4613 update=update_in_front,
4615 SURFSK_show_wire: BoolProperty(
4616 name="Show wire",
4617 description="Add the object’s wireframe over solid drawing",
4618 default=False,
4619 update=update_show_wire,
4621 SURFSK_shade_smooth: BoolProperty(
4622 name="Shade smooth",
4623 description="Render and display faces smooth, using interpolated Vertex Normals",
4624 default=False,
4625 update=update_shade_smooth,
4628 classes = (
4629 MESH_OT_SURFSK_init,
4630 MESH_OT_SURFSK_add_modifiers,
4631 MESH_OT_SURFSK_add_surface,
4632 MESH_OT_SURFSK_edit_surface,
4633 GPENCIL_OT_SURFSK_add_strokes,
4634 GPENCIL_OT_SURFSK_edit_strokes,
4635 GPENCIL_OT_SURFSK_strokes_to_curves,
4636 GPENCIL_OT_SURFSK_annotation_to_curves,
4637 GPENCIL_OT_SURFSK_add_annotation,
4638 CURVE_OT_SURFSK_edit_curve,
4639 CURVE_OT_SURFSK_reorder_splines,
4640 CURVE_OT_SURFSK_first_points,
4641 BsurfPreferences,
4642 BsurfacesProps
4645 def register():
4646 for cls in classes:
4647 bpy.utils.register_class(cls)
4649 for panel in panels:
4650 bpy.utils.register_class(panel)
4652 bpy.types.Scene.bsurfaces = PointerProperty(type=BsurfacesProps)
4653 update_panel(None, bpy.context)
4655 def unregister():
4656 for panel in panels:
4657 bpy.utils.unregister_class(panel)
4659 for cls in classes:
4660 bpy.utils.unregister_class(cls)
4662 del bpy.types.Scene.bsurfaces
4664 if __name__ == "__main__":
4665 register()