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
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 #####
21 "name": "Bsurfaces GPL Edition",
22 "author": "Eclectiel, Spivak Vladimir(cwolf3d)",
24 "blender": (2, 80, 0),
25 "location": "View3D > EditMode > ToolShelf",
26 "description": "Modeling and retopology tool",
27 "wiki_url": "https://wiki.blender.org/index.php/Dev:Ref/Release_Notes/2.64/Bsurfaces_1.5",
34 from bpy_extras
import object_utils
37 from mathutils
import Matrix
, Vector
38 from mathutils
.geometry
import (
47 from bpy
.props
import (
54 from bpy
.types
import (
62 class VIEW3D_PT_tools_SURFSK_mesh(Panel
):
63 bl_space_type
= 'VIEW_3D'
66 #bl_context = "mesh_edit"
67 bl_label
= "Bsurfaces"
69 def draw(self
, context
):
71 scn
= context
.scene
.bsurfaces
73 col
= layout
.column(align
=True)
76 col
.operator("gpencil.surfsk_init", text
="Initialize")
77 col
.prop(scn
, "SURFSK_object_with_retopology")
78 col
.prop(scn
, "SURFSK_object_with_strokes")
80 col
.operator("gpencil.surfsk_add_surface", text
="Add Surface")
81 col
.operator("gpencil.surfsk_add_strokes", text
="Add Strokes")
82 col
.operator("gpencil.surfsk_edit_strokes", text
="Edit Strokes")
83 col
.prop(scn
, "SURFSK_cyclic_cross")
84 col
.prop(scn
, "SURFSK_cyclic_follow")
85 col
.prop(scn
, "SURFSK_loops_on_strokes")
86 col
.prop(scn
, "SURFSK_automatic_join")
87 col
.prop(scn
, "SURFSK_keep_strokes")
89 class VIEW3D_PT_tools_SURFSK_curve(Panel
):
90 bl_space_type
= 'VIEW_3D'
92 bl_context
= "curve_edit"
94 bl_label
= "Bsurfaces"
97 def poll(cls
, context
):
98 return context
.active_object
100 def draw(self
, context
):
103 col
= layout
.column(align
=True)
106 col
.operator("curve.surfsk_first_points", text
="Set First Points")
107 col
.operator("curve.switch_direction", text
="Switch Direction")
108 col
.operator("curve.surfsk_reorder_splines", text
="Reorder Splines")
111 # Returns the type of strokes used
112 def get_strokes_type():
116 # Check if they are grease pencil
118 gpencil
= bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
119 layer
= gpencil
.data
.layers
[0]
120 frame
= layer
.frames
[0]
122 strokes_num
= len(frame
.strokes
)
125 strokes_type
= "GP_STROKES"
129 # Check if they are mesh
131 main_object
= bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_retopology
135 # Check if they are curves, if there aren't grease pencil strokes
136 if strokes_type
== "":
137 if len(bpy
.context
.selected_objects
) == 2:
138 for ob
in bpy
.context
.selected_objects
:
139 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.type == "CURVE":
140 strokes_type
= "EXTERNAL_CURVE"
141 strokes_num
= len(ob
.data
.splines
)
143 # Check if there is any non-bezier spline
144 for i
in range(len(ob
.data
.splines
)):
145 if ob
.data
.splines
[i
].type != "BEZIER":
146 strokes_type
= "CURVE_WITH_NON_BEZIER_SPLINES"
149 elif ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.type != "CURVE":
150 strokes_type
= "EXTERNAL_NO_CURVE"
151 elif len(bpy
.context
.selected_objects
) > 2:
152 strokes_type
= "MORE_THAN_ONE_EXTERNAL"
154 # Check if there is a single stroke without any selection in the object
155 if strokes_num
== 1 and main_object
.data
.total_vert_sel
== 0:
156 if strokes_type
== "EXTERNAL_CURVE":
157 strokes_type
= "SINGLE_CURVE_STROKE_NO_SELECTION"
158 elif strokes_type
== "GP_STROKES":
159 strokes_type
= "SINGLE_GP_STROKE_NO_SELECTION"
161 if strokes_num
== 0 and main_object
.data
.total_vert_sel
> 0:
162 strokes_type
= "SELECTION_ALONE"
164 if strokes_type
== "":
165 strokes_type
= "NO_STROKES"
170 # Surface generator operator
171 class GPENCIL_OT_SURFSK_add_surface(Operator
):
172 bl_idname
= "gpencil.surfsk_add_surface"
173 bl_label
= "Bsurfaces add surface"
174 bl_description
= "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
175 bl_options
= {'REGISTER', 'UNDO'}
177 is_fill_faces
: BoolProperty(
180 selection_U_exists
: BoolProperty(
183 selection_V_exists
: BoolProperty(
186 selection_U2_exists
: BoolProperty(
189 selection_V2_exists
: BoolProperty(
192 selection_V_is_closed
: BoolProperty(
195 selection_U_is_closed
: BoolProperty(
198 selection_V2_is_closed
: BoolProperty(
201 selection_U2_is_closed
: BoolProperty(
205 edges_U
: IntProperty(
207 description
="Number of face-loops crossing the strokes",
212 edges_V
: IntProperty(
214 description
="Number of face-loops following the strokes",
219 cyclic_cross
: BoolProperty(
221 description
="Make cyclic the face-loops crossing the strokes",
224 cyclic_follow
: BoolProperty(
225 name
="Cyclic Follow",
226 description
="Make cyclic the face-loops following the strokes",
229 loops_on_strokes
: BoolProperty(
230 name
="Loops on strokes",
231 description
="Make the loops match the paths of the strokes",
234 automatic_join
: BoolProperty(
235 name
="Automatic join",
236 description
="Join automatically vertices of either surfaces generated "
237 "by crosshatching, or from the borders of closed shapes",
240 join_stretch_factor
: FloatProperty(
242 description
="Amount of stretching or shrinking allowed for "
243 "edges when joining vertices automatically",
249 keep_strokes
: BoolProperty(
251 description
="Keeps the sketched strokes or curves after adding the surface",
254 strokes_type
: StringProperty()
255 initial_global_undo_state
: BoolProperty()
258 def draw(self
, context
):
260 col
= layout
.column(align
=True)
263 if not self
.is_fill_faces
:
265 if not self
.is_crosshatch
:
266 if not self
.selection_U_exists
:
267 col
.prop(self
, "edges_U")
270 if not self
.selection_V_exists
:
271 col
.prop(self
, "edges_V")
276 if not self
.selection_U_exists
:
278 (self
.selection_V_exists
and not self
.selection_V_is_closed
) or
279 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)
281 col
.prop(self
, "cyclic_cross")
283 if not self
.selection_V_exists
:
285 (self
.selection_U_exists
and not self
.selection_U_is_closed
) or
286 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)
288 col
.prop(self
, "cyclic_follow")
290 col
.prop(self
, "loops_on_strokes")
292 col
.prop(self
, "automatic_join")
294 if self
.automatic_join
:
298 col
.prop(self
, "join_stretch_factor")
300 col
.prop(self
, "keep_strokes")
302 # Get an ordered list of a chain of vertices
303 def get_ordered_verts(self
, ob
, all_selected_edges_idx
, all_selected_verts_idx
,
304 first_vert_idx
, middle_vertex_idx
, closing_vert_idx
):
305 # Order selected vertices.
307 if closing_vert_idx
is not None:
308 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
310 verts_ordered
.append(ob
.data
.vertices
[first_vert_idx
])
311 prev_v
= first_vert_idx
315 edges_non_matched
= 0
316 for i
in all_selected_edges_idx
:
317 if ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[0] == prev_v
and \
318 ob
.data
.edges
[i
].vertices
[1] in all_selected_verts_idx
:
320 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[1]])
321 prev_v
= ob
.data
.edges
[i
].vertices
[1]
322 prev_ed
= ob
.data
.edges
[i
]
323 elif ob
.data
.edges
[i
] != prev_ed
and ob
.data
.edges
[i
].vertices
[1] == prev_v
and \
324 ob
.data
.edges
[i
].vertices
[0] in all_selected_verts_idx
:
326 verts_ordered
.append(ob
.data
.vertices
[ob
.data
.edges
[i
].vertices
[0]])
327 prev_v
= ob
.data
.edges
[i
].vertices
[0]
328 prev_ed
= ob
.data
.edges
[i
]
330 edges_non_matched
+= 1
332 if edges_non_matched
== len(all_selected_edges_idx
):
338 if closing_vert_idx
is not None:
339 verts_ordered
.append(ob
.data
.vertices
[closing_vert_idx
])
341 if middle_vertex_idx
is not None:
342 verts_ordered
.append(ob
.data
.vertices
[middle_vertex_idx
])
343 verts_ordered
.reverse()
345 return tuple(verts_ordered
)
347 # Calculates length of a chain of points.
348 def get_chain_length(self
, object, verts_ordered
):
349 matrix
= object.matrix_world
352 edges_lengths_sum
= 0
353 for i
in range(0, len(verts_ordered
)):
355 prev_v_co
= matrix
@ verts_ordered
[i
].co
357 v_co
= matrix
@ verts_ordered
[i
].co
359 v_difs
= [prev_v_co
[0] - v_co
[0], prev_v_co
[1] - v_co
[1], prev_v_co
[2] - v_co
[2]]
360 edge_length
= abs(sqrt(v_difs
[0] * v_difs
[0] + v_difs
[1] * v_difs
[1] + v_difs
[2] * v_difs
[2]))
362 edges_lengths
.append(edge_length
)
363 edges_lengths_sum
+= edge_length
367 return edges_lengths
, edges_lengths_sum
369 # Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
370 def get_edges_proportions(self
, edges_lengths
, edges_lengths_sum
, use_boundaries
, fixed_edges_num
):
371 edges_proportions
= []
374 for l
in edges_lengths
:
375 edges_proportions
.append(l
/ edges_lengths_sum
)
379 for n
in range(0, fixed_edges_num
):
380 edges_proportions
.append(1 / fixed_edges_num
)
383 return edges_proportions
385 # Calculates the angle between two pairs of points in space
386 def orientation_difference(self
, points_A_co
, points_B_co
):
387 # each parameter should be a list with two elements,
388 # and each element should be a x,y,z coordinate
389 vec_A
= points_A_co
[0] - points_A_co
[1]
390 vec_B
= points_B_co
[0] - points_B_co
[1]
392 angle
= vec_A
.angle(vec_B
)
395 angle
= abs(angle
- pi
)
399 # Calculate the which vert of verts_idx list is the nearest one
400 # to the point_co coordinates, and the distance
401 def shortest_distance(self
, object, point_co
, verts_idx
):
402 matrix
= object.matrix_world
404 for i
in range(0, len(verts_idx
)):
405 dist
= (point_co
- matrix
@ object.data
.vertices
[verts_idx
[i
]].co
).length
408 nearest_vert_idx
= verts_idx
[i
]
413 nearest_vert_idx
= verts_idx
[i
]
416 return nearest_vert_idx
, shortest_dist
418 # Returns the index of the opposite vert tip in a chain, given a vert tip index
419 # as parameter, and a multidimentional list with all pairs of tips
420 def opposite_tip(self
, vert_tip_idx
, all_chains_tips_idx
):
421 opposite_vert_tip_idx
= None
422 for i
in range(0, len(all_chains_tips_idx
)):
423 if vert_tip_idx
== all_chains_tips_idx
[i
][0]:
424 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][1]
425 if vert_tip_idx
== all_chains_tips_idx
[i
][1]:
426 opposite_vert_tip_idx
= all_chains_tips_idx
[i
][0]
428 return opposite_vert_tip_idx
430 # Simplifies a spline and returns the new points coordinates
431 def simplify_spline(self
, spline_coords
, segments_num
):
432 simplified_spline
= []
433 points_between_segments
= round(len(spline_coords
) / segments_num
)
435 simplified_spline
.append(spline_coords
[0])
436 for i
in range(1, segments_num
):
437 simplified_spline
.append(spline_coords
[i
* points_between_segments
])
439 simplified_spline
.append(spline_coords
[len(spline_coords
) - 1])
441 return simplified_spline
443 # Cleans up the scene and gets it the same it was at the beginning,
444 # in case the script is interrupted in the middle of the execution
445 def cleanup_on_interruption(self
):
446 # If the original strokes curve comes from conversion
447 # from grease pencil and wasn't made by hand, delete it
448 if not self
.using_external_curves
:
450 bpy
.ops
.object.delete({"selected_objects": [self
.original_curve
]})
454 bpy
.ops
.object.delete({"selected_objects": [self
.main_object
]})
456 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
457 self
.original_curve
.select_set(True)
458 self
.main_object
.select_set(True)
459 bpy
.context
.view_layer
.objects
.active
= self
.main_object
461 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
463 # Returns a list with the coords of the points distributed over the splines
464 # passed to this method according to the proportions parameter
465 def distribute_pts(self
, surface_splines
, proportions
):
467 # Calculate the length of each final surface spline
468 surface_splines_lengths
= []
469 surface_splines_parsed
= []
471 for sp_idx
in range(0, len(surface_splines
)):
472 # Calculate spline length
473 surface_splines_lengths
.append(0)
475 for i
in range(0, len(surface_splines
[sp_idx
].bezier_points
)):
477 prev_p
= surface_splines
[sp_idx
].bezier_points
[i
]
479 p
= surface_splines
[sp_idx
].bezier_points
[i
]
480 edge_length
= (prev_p
.co
- p
.co
).length
481 surface_splines_lengths
[sp_idx
] += edge_length
485 # Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
486 for sp_idx
in range(0, len(surface_splines
)):
487 surface_splines_parsed
.append([])
488 surface_splines_parsed
[sp_idx
].append(surface_splines
[sp_idx
].bezier_points
[0].co
)
490 prev_p_co
= surface_splines
[sp_idx
].bezier_points
[0].co
493 for prop_idx
in range(len(proportions
) - 1):
494 target_length
= surface_splines_lengths
[sp_idx
] * proportions
[prop_idx
]
495 partial_segment_length
= 0
499 # if not it'll pass the p_idx as an index below and crash
500 if p_idx
< len(surface_splines
[sp_idx
].bezier_points
):
501 p_co
= surface_splines
[sp_idx
].bezier_points
[p_idx
].co
502 new_dist
= (prev_p_co
- p_co
).length
504 # The new distance that could have the partial segment if
505 # it is still shorter than the target length
506 potential_segment_length
= partial_segment_length
+ new_dist
508 # If the potential is still shorter, keep adding
509 if potential_segment_length
< target_length
:
510 partial_segment_length
= potential_segment_length
515 # If the potential is longer than the target, calculate the target
516 # (a point between the last two points), and assign
517 elif potential_segment_length
> target_length
:
518 remaining_dist
= target_length
- partial_segment_length
519 vec
= p_co
- prev_p_co
521 intermediate_co
= prev_p_co
+ (vec
* remaining_dist
)
523 surface_splines_parsed
[sp_idx
].append(intermediate_co
)
525 partial_segment_length
+= remaining_dist
526 prev_p_co
= intermediate_co
530 # If the potential is equal to the target, assign
531 elif potential_segment_length
== target_length
:
532 surface_splines_parsed
[sp_idx
].append(p_co
)
540 # last point of the spline
541 surface_splines_parsed
[sp_idx
].append(
542 surface_splines
[sp_idx
].bezier_points
[len(surface_splines
[sp_idx
].bezier_points
) - 1].co
545 return surface_splines_parsed
547 # Counts the number of faces that belong to each edge
548 def edge_face_count(self
, ob
):
549 ed_keys_count_dict
= {}
551 for face
in ob
.data
.polygons
:
552 for ed_keys
in face
.edge_keys
:
553 if ed_keys
not in ed_keys_count_dict
:
554 ed_keys_count_dict
[ed_keys
] = 1
556 ed_keys_count_dict
[ed_keys
] += 1
559 for i
in range(len(ob
.data
.edges
)):
560 edge_face_count
.append(0)
562 for i
in range(len(ob
.data
.edges
)):
563 ed
= ob
.data
.edges
[i
]
568 if (v1
, v2
) in ed_keys_count_dict
:
569 edge_face_count
[i
] = ed_keys_count_dict
[(v1
, v2
)]
570 elif (v2
, v1
) in ed_keys_count_dict
:
571 edge_face_count
[i
] = ed_keys_count_dict
[(v2
, v1
)]
573 return edge_face_count
575 # Fills with faces all the selected vertices which form empty triangles or quads
576 def fill_with_faces(self
, object):
577 all_selected_verts_count
= self
.main_object_selected_verts_count
579 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
581 # Calculate average length of selected edges
582 all_selected_verts
= []
583 original_sel_edges_count
= 0
584 for ed
in object.data
.edges
:
585 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
587 coords
.append(object.data
.vertices
[ed
.vertices
[0]].co
)
588 coords
.append(object.data
.vertices
[ed
.vertices
[1]].co
)
590 original_sel_edges_count
+= 1
592 if not ed
.vertices
[0] in all_selected_verts
:
593 all_selected_verts
.append(ed
.vertices
[0])
595 if not ed
.vertices
[1] in all_selected_verts
:
596 all_selected_verts
.append(ed
.vertices
[1])
598 tuple(all_selected_verts
)
600 # Check if there is any edge selected. If not, interrupt the script
601 if original_sel_edges_count
== 0 and all_selected_verts_count
> 0:
604 # Get all edges connected to selected verts
605 all_edges_around_sel_verts
= []
606 edges_connected_to_sel_verts
= {}
607 verts_connected_to_every_vert
= {}
608 for ed_idx
in range(len(object.data
.edges
)):
609 ed
= object.data
.edges
[ed_idx
]
612 if ed
.vertices
[0] in all_selected_verts
:
613 if not ed
.vertices
[0] in edges_connected_to_sel_verts
:
614 edges_connected_to_sel_verts
[ed
.vertices
[0]] = []
616 edges_connected_to_sel_verts
[ed
.vertices
[0]].append(ed_idx
)
619 if ed
.vertices
[1] in all_selected_verts
:
620 if not ed
.vertices
[1] in edges_connected_to_sel_verts
:
621 edges_connected_to_sel_verts
[ed
.vertices
[1]] = []
623 edges_connected_to_sel_verts
[ed
.vertices
[1]].append(ed_idx
)
626 if include_edge
is True:
627 all_edges_around_sel_verts
.append(ed_idx
)
629 # Get all connected verts to each vert
630 if not ed
.vertices
[0] in verts_connected_to_every_vert
:
631 verts_connected_to_every_vert
[ed
.vertices
[0]] = []
633 if not ed
.vertices
[1] in verts_connected_to_every_vert
:
634 verts_connected_to_every_vert
[ed
.vertices
[1]] = []
636 verts_connected_to_every_vert
[ed
.vertices
[0]].append(ed
.vertices
[1])
637 verts_connected_to_every_vert
[ed
.vertices
[1]].append(ed
.vertices
[0])
639 # Get all verts connected to faces
640 all_verts_part_of_faces
= []
641 all_edges_faces_count
= []
642 all_edges_faces_count
+= self
.edge_face_count(object)
644 # Get only the selected edges that have faces attached.
645 count_faces_of_edges_around_sel_verts
= {}
646 selected_verts_with_faces
= []
647 for ed_idx
in all_edges_around_sel_verts
:
648 count_faces_of_edges_around_sel_verts
[ed_idx
] = all_edges_faces_count
[ed_idx
]
650 if all_edges_faces_count
[ed_idx
] > 0:
651 ed
= object.data
.edges
[ed_idx
]
653 if not ed
.vertices
[0] in selected_verts_with_faces
:
654 selected_verts_with_faces
.append(ed
.vertices
[0])
656 if not ed
.vertices
[1] in selected_verts_with_faces
:
657 selected_verts_with_faces
.append(ed
.vertices
[1])
659 all_verts_part_of_faces
.append(ed
.vertices
[0])
660 all_verts_part_of_faces
.append(ed
.vertices
[1])
662 tuple(selected_verts_with_faces
)
664 # Discard unneeded verts from calculations
665 participating_verts
= []
667 for v_idx
in all_selected_verts
:
668 vert_has_edges_with_one_face
= False
670 # Check if the actual vert has at least one edge connected to only one face
671 for ed_idx
in edges_connected_to_sel_verts
[v_idx
]:
672 if count_faces_of_edges_around_sel_verts
[ed_idx
] == 1:
673 vert_has_edges_with_one_face
= True
675 # If the vert has two or less edges connected and the vert is not part of any face.
676 # Or the vert is part of any face and at least one of
677 # the connected edges has only one face attached to it.
678 if (len(edges_connected_to_sel_verts
[v_idx
]) == 2 and
679 v_idx
not in all_verts_part_of_faces
) or \
680 len(edges_connected_to_sel_verts
[v_idx
]) == 1 or \
681 (v_idx
in all_verts_part_of_faces
and
682 vert_has_edges_with_one_face
):
684 participating_verts
.append(v_idx
)
686 if v_idx
not in all_verts_part_of_faces
:
687 movable_verts
.append(v_idx
)
689 # Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
690 for mv_idx
in movable_verts
:
692 mv_connected_verts
= verts_connected_to_every_vert
[mv_idx
]
694 for actual_v_idx
in all_selected_verts
:
695 count_shared_neighbors
= 0
698 for mv_conn_v_idx
in mv_connected_verts
:
699 if mv_idx
!= actual_v_idx
:
700 if mv_conn_v_idx
in verts_connected_to_every_vert
[actual_v_idx
] and \
701 mv_conn_v_idx
not in checked_verts
:
702 count_shared_neighbors
+= 1
703 checked_verts
.append(mv_conn_v_idx
)
705 if actual_v_idx
in mv_connected_verts
:
709 if count_shared_neighbors
== 2:
717 movable_verts
.remove(mv_idx
)
719 # Calculate merge distance for participating verts
720 shortest_edge_length
= None
721 for ed
in object.data
.edges
:
722 if ed
.vertices
[0] in movable_verts
and ed
.vertices
[1] in movable_verts
:
723 v1
= object.data
.vertices
[ed
.vertices
[0]]
724 v2
= object.data
.vertices
[ed
.vertices
[1]]
726 length
= (v1
.co
- v2
.co
).length
728 if shortest_edge_length
is None:
729 shortest_edge_length
= length
731 if length
< shortest_edge_length
:
732 shortest_edge_length
= length
734 if shortest_edge_length
is not None:
735 edges_merge_distance
= shortest_edge_length
* 0.5
737 edges_merge_distance
= 0
739 # Get together the verts near enough. They will be merged later
741 remaining_verts
+= participating_verts
742 for v1_idx
in participating_verts
:
743 if v1_idx
in remaining_verts
and v1_idx
in movable_verts
:
745 coords_verts_to_merge
= {}
747 verts_to_merge
.append(v1_idx
)
749 v1_co
= object.data
.vertices
[v1_idx
].co
750 coords_verts_to_merge
[v1_idx
] = (v1_co
[0], v1_co
[1], v1_co
[2])
752 for v2_idx
in remaining_verts
:
754 v2_co
= object.data
.vertices
[v2_idx
].co
756 dist
= (v1_co
- v2_co
).length
758 if dist
<= edges_merge_distance
: # Add the verts which are near enough
759 verts_to_merge
.append(v2_idx
)
761 coords_verts_to_merge
[v2_idx
] = (v2_co
[0], v2_co
[1], v2_co
[2])
763 for vm_idx
in verts_to_merge
:
764 remaining_verts
.remove(vm_idx
)
766 if len(verts_to_merge
) > 1:
767 # Calculate middle point of the verts to merge.
771 movable_verts_to_merge_count
= 0
772 for i
in range(len(verts_to_merge
)):
773 if verts_to_merge
[i
] in movable_verts
:
774 v_co
= object.data
.vertices
[verts_to_merge
[i
]].co
780 movable_verts_to_merge_count
+= 1
783 sum_x_co
/ movable_verts_to_merge_count
,
784 sum_y_co
/ movable_verts_to_merge_count
,
785 sum_z_co
/ movable_verts_to_merge_count
788 # Check if any vert to be merged is not movable
790 are_verts_not_movable
= False
791 verts_not_movable
= []
792 for v_merge_idx
in verts_to_merge
:
793 if v_merge_idx
in participating_verts
and v_merge_idx
not in movable_verts
:
794 are_verts_not_movable
= True
795 verts_not_movable
.append(v_merge_idx
)
797 if are_verts_not_movable
:
798 # Get the vert connected to faces, that is nearest to
799 # the middle point of the movable verts
801 for vcf_idx
in verts_not_movable
:
802 dist
= abs((object.data
.vertices
[vcf_idx
].co
-
803 Vector(middle_point_co
)).length
)
805 if shortest_dist
is None:
807 nearest_vert_idx
= vcf_idx
809 if dist
< shortest_dist
:
811 nearest_vert_idx
= vcf_idx
813 coords
= object.data
.vertices
[nearest_vert_idx
].co
814 target_point_co
= [coords
[0], coords
[1], coords
[2]]
816 target_point_co
= middle_point_co
818 # Move verts to merge to the middle position
819 for v_merge_idx
in verts_to_merge
:
820 if v_merge_idx
in movable_verts
: # Only move the verts that are not part of faces
821 object.data
.vertices
[v_merge_idx
].co
[0] = target_point_co
[0]
822 object.data
.vertices
[v_merge_idx
].co
[1] = target_point_co
[1]
823 object.data
.vertices
[v_merge_idx
].co
[2] = target_point_co
[2]
825 # Perform "Remove Doubles" to weld all the disconnected verts
826 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
827 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
829 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
831 # Get all the definitive selected edges, after weldding
833 edges_per_vert
= {} # Number of faces of each selected edge
834 for ed
in object.data
.edges
:
835 if object.data
.vertices
[ed
.vertices
[0]].select
and object.data
.vertices
[ed
.vertices
[1]].select
:
836 selected_edges
.append(ed
.index
)
838 # Save all the edges that belong to each vertex.
839 if not ed
.vertices
[0] in edges_per_vert
:
840 edges_per_vert
[ed
.vertices
[0]] = []
842 if not ed
.vertices
[1] in edges_per_vert
:
843 edges_per_vert
[ed
.vertices
[1]] = []
845 edges_per_vert
[ed
.vertices
[0]].append(ed
.index
)
846 edges_per_vert
[ed
.vertices
[1]].append(ed
.index
)
848 # Check if all the edges connected to each vert have two faces attached to them.
849 # To discard them later and make calculations faster
851 a
+= self
.edge_face_count(object)
853 verts_surrounded_by_faces
= {}
854 for v_idx
in edges_per_vert
:
855 edges
= edges_per_vert
[v_idx
]
856 edges_with_two_faces_count
= 0
858 for ed_idx
in edges_per_vert
[v_idx
]:
860 edges_with_two_faces_count
+= 1
862 if edges_with_two_faces_count
== len(edges_per_vert
[v_idx
]):
863 verts_surrounded_by_faces
[v_idx
] = True
865 verts_surrounded_by_faces
[v_idx
] = False
867 # Get all the selected vertices
868 selected_verts_idx
= []
869 for v
in object.data
.vertices
:
871 selected_verts_idx
.append(v
.index
)
873 # Get all the faces of the object
874 all_object_faces_verts_idx
= []
875 for face
in object.data
.polygons
:
877 face_verts
.append(face
.vertices
[0])
878 face_verts
.append(face
.vertices
[1])
879 face_verts
.append(face
.vertices
[2])
881 if len(face
.vertices
) == 4:
882 face_verts
.append(face
.vertices
[3])
884 all_object_faces_verts_idx
.append(face_verts
)
886 # Deselect all vertices
887 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
888 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
889 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
891 # Make a dictionary with the verts related to each vert
892 related_key_verts
= {}
893 for ed_idx
in selected_edges
:
894 ed
= object.data
.edges
[ed_idx
]
896 if not verts_surrounded_by_faces
[ed
.vertices
[0]]:
897 if not ed
.vertices
[0] in related_key_verts
:
898 related_key_verts
[ed
.vertices
[0]] = []
900 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
901 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
903 if not verts_surrounded_by_faces
[ed
.vertices
[1]]:
904 if not ed
.vertices
[1] in related_key_verts
:
905 related_key_verts
[ed
.vertices
[1]] = []
907 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
908 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
910 # Get groups of verts forming each face
912 for v1
in related_key_verts
: # verts-1 ....
913 for v2
in related_key_verts
: # verts-2
915 related_verts_in_common
= []
918 for rel_v1
in related_key_verts
[v1
]:
919 # Check if related verts of verts-1 are related verts of verts-2
920 if rel_v1
in related_key_verts
[v2
]:
921 related_verts_in_common
.append(rel_v1
)
923 if v2
in related_key_verts
[v1
]:
926 if v1
in related_key_verts
[v2
]:
929 repeated_face
= False
930 # If two verts have two related verts in common, they form a quad
931 if len(related_verts_in_common
) == 2:
932 # Check if the face is already saved
933 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
935 for f_verts
in all_faces_to_check_idx
:
938 if len(f_verts
) == 4:
943 if related_verts_in_common
[0] in f_verts
:
945 if related_verts_in_common
[1] in f_verts
:
948 if repeated_verts
== len(f_verts
):
952 if not repeated_face
:
953 faces_verts_idx
.append(
954 [v1
, related_verts_in_common
[0], v2
, related_verts_in_common
[1]]
957 # If Two verts have one related vert in common and
958 # they are related to each other, they form a triangle
959 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
960 # Check if the face is already saved.
961 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
963 for f_verts
in all_faces_to_check_idx
:
966 if len(f_verts
) == 3:
971 if related_verts_in_common
[0] in f_verts
:
974 if repeated_verts
== len(f_verts
):
978 if not repeated_face
:
979 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
981 # Keep only the faces that don't overlap by ignoring quads
982 # that overlap with two adjacent triangles
983 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
984 all_faces_to_check_idx
= faces_verts_idx
+ all_object_faces_verts_idx
985 for i
in range(len(faces_verts_idx
)):
986 for t
in range(len(all_faces_to_check_idx
)):
990 if len(faces_verts_idx
[i
]) == 4 and len(all_faces_to_check_idx
[t
]) == 3:
991 for v_idx
in all_faces_to_check_idx
[t
]:
992 if v_idx
in faces_verts_idx
[i
]:
994 # If it doesn't have all it's vertices repeated in the other face
995 if verts_in_common
== 3:
996 if i
not in faces_to_not_include_idx
:
997 faces_to_not_include_idx
.append(i
)
999 # Build faces discarding the ones in faces_to_not_include
1004 num_faces_created
= 0
1005 for i
in range(len(faces_verts_idx
)):
1006 if i
not in faces_to_not_include_idx
:
1007 bm
.faces
.new([bm
.verts
[v
] for v
in faces_verts_idx
[i
]])
1009 num_faces_created
+= 1
1014 for v_idx
in selected_verts_idx
:
1015 self
.main_object
.data
.vertices
[v_idx
].select
= True
1017 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='EDIT')
1018 bpy
.ops
.mesh
.normals_make_consistent(inside
=False)
1019 bpy
.ops
.object.mode_set('INVOKE_REGION_WIN', mode
='OBJECT')
1021 return num_faces_created
1023 # Crosshatch skinning
1024 def crosshatch_surface_invoke(self
, ob_original_splines
):
1025 self
.is_crosshatch
= False
1026 self
.crosshatch_merge_distance
= 0
1028 objects_to_delete
= [] # duplicated strokes to be deleted.
1030 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1031 # (without this the surface verts merging with the main object doesn't work well)
1032 self
.modifiers_prev_viewport_state
= []
1033 if len(self
.main_object
.modifiers
) > 0:
1034 for m_idx
in range(len(self
.main_object
.modifiers
)):
1035 self
.modifiers_prev_viewport_state
.append(
1036 self
.main_object
.modifiers
[m_idx
].show_viewport
1038 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1040 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1041 ob_original_splines
.select_set(True)
1042 bpy
.context
.view_layer
.objects
.active
= ob_original_splines
1044 if len(ob_original_splines
.data
.splines
) >= 2:
1045 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1046 ob_splines
= bpy
.context
.object
1047 ob_splines
.name
= "SURFSKIO_NE_STR"
1049 # Get estimative merge distance (sum up the distances from the first point to
1050 # all other points, then average them and then divide them)
1051 first_point_dist_sum
= 0
1054 coords_first_pt
= ob_splines
.data
.splines
[0].bezier_points
[0].co
1055 for i
in range(len(ob_splines
.data
.splines
)):
1056 sp
= ob_splines
.data
.splines
[i
]
1058 if coords_first_pt
!= sp
.bezier_points
[0].co
:
1059 first_dist
= (coords_first_pt
- sp
.bezier_points
[0].co
).length
1061 if coords_first_pt
!= sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
:
1062 second_dist
= (coords_first_pt
- sp
.bezier_points
[len(sp
.bezier_points
) - 1].co
).length
1064 first_point_dist_sum
+= first_dist
+ second_dist
1068 shortest_dist
= first_dist
1069 elif second_dist
!= 0:
1070 shortest_dist
= second_dist
1072 if shortest_dist
> first_dist
and first_dist
!= 0:
1073 shortest_dist
= first_dist
1075 if shortest_dist
> second_dist
and second_dist
!= 0:
1076 shortest_dist
= second_dist
1078 self
.crosshatch_merge_distance
= shortest_dist
/ 20
1080 # Recalculation of merge distance
1082 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1084 ob_calc_merge_dist
= bpy
.context
.object
1085 ob_calc_merge_dist
.name
= "SURFSKIO_CALC_TMP"
1087 objects_to_delete
.append(ob_calc_merge_dist
)
1089 # Smooth out strokes a little to improve crosshatch detection
1090 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1091 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1094 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1096 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1097 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1099 # Convert curves into mesh
1100 ob_calc_merge_dist
.data
.resolution_u
= 12
1101 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
1103 # Find "intersection-nodes"
1104 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1105 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1106 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1107 threshold
=self
.crosshatch_merge_distance
)
1108 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1109 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1111 # Remove verts with less than three edges
1112 verts_edges_count
= {}
1113 for ed
in ob_calc_merge_dist
.data
.edges
:
1116 if v
[0] not in verts_edges_count
:
1117 verts_edges_count
[v
[0]] = 0
1119 if v
[1] not in verts_edges_count
:
1120 verts_edges_count
[v
[1]] = 0
1122 verts_edges_count
[v
[0]] += 1
1123 verts_edges_count
[v
[1]] += 1
1125 nodes_verts_coords
= []
1126 for v_idx
in verts_edges_count
:
1127 v
= ob_calc_merge_dist
.data
.vertices
[v_idx
]
1129 if verts_edges_count
[v_idx
] < 3:
1133 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1134 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
1135 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1137 # Remove doubles to discard very near verts from calculations of distance
1138 bpy
.ops
.mesh
.remove_doubles(
1139 'INVOKE_REGION_WIN',
1140 threshold
=self
.crosshatch_merge_distance
* 4.0
1142 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1144 # Get all coords of the resulting nodes
1145 nodes_verts_coords
= [(v
.co
[0], v
.co
[1], v
.co
[2]) for
1146 v
in ob_calc_merge_dist
.data
.vertices
]
1148 # Check if the strokes are a crosshatch
1149 if len(nodes_verts_coords
) >= 3:
1150 self
.is_crosshatch
= True
1152 shortest_dist
= None
1153 for co_1
in nodes_verts_coords
:
1154 for co_2
in nodes_verts_coords
:
1156 dist
= (Vector(co_1
) - Vector(co_2
)).length
1158 if shortest_dist
is not None:
1159 if dist
< shortest_dist
:
1160 shortest_dist
= dist
1162 shortest_dist
= dist
1164 self
.crosshatch_merge_distance
= shortest_dist
/ 3
1166 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1167 ob_splines
.select_set(True)
1168 bpy
.context
.view_layer
.objects
.active
= ob_splines
1170 # Deselect all points
1171 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1172 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1173 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1175 # Smooth splines in a localized way, to eliminate "saw-teeth"
1176 # like shapes when there are many points
1177 for sp
in ob_splines
.data
.splines
:
1180 angle_limit
= 2 # Degrees
1181 for t
in range(len(sp
.bezier_points
)):
1182 # Because on each iteration it checks the "next two points"
1183 # of the actual. This way it doesn't go out of range
1184 if t
<= len(sp
.bezier_points
) - 3:
1185 p1
= sp
.bezier_points
[t
]
1186 p2
= sp
.bezier_points
[t
+ 1]
1187 p3
= sp
.bezier_points
[t
+ 2]
1189 vec_1
= p1
.co
- p2
.co
1190 vec_2
= p2
.co
- p3
.co
1192 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1193 angle
= vec_1
.angle(vec_2
)
1194 angle_sum
+= degrees(angle
)
1196 if angle_sum
>= angle_limit
: # If sum of angles is grater than the limit
1197 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1198 p1
.select_control_point
= True
1199 p1
.select_left_handle
= True
1200 p1
.select_right_handle
= True
1202 p2
.select_control_point
= True
1203 p2
.select_left_handle
= True
1204 p2
.select_right_handle
= True
1206 if (p1
.co
- p2
.co
).length
<= self
.crosshatch_merge_distance
:
1207 p3
.select_control_point
= True
1208 p3
.select_left_handle
= True
1209 p3
.select_right_handle
= True
1213 sp
.bezier_points
[0].select_control_point
= False
1214 sp
.bezier_points
[0].select_left_handle
= False
1215 sp
.bezier_points
[0].select_right_handle
= False
1217 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= False
1218 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= False
1219 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= False
1221 # Smooth out strokes a little to improve crosshatch detection
1222 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1225 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1227 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1228 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1230 # Simplify the splines
1231 for sp
in ob_splines
.data
.splines
:
1234 sp
.bezier_points
[0].select_control_point
= True
1235 sp
.bezier_points
[0].select_left_handle
= True
1236 sp
.bezier_points
[0].select_right_handle
= True
1238 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_control_point
= True
1239 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_left_handle
= True
1240 sp
.bezier_points
[len(sp
.bezier_points
) - 1].select_right_handle
= True
1242 angle_limit
= 15 # Degrees
1243 for t
in range(len(sp
.bezier_points
)):
1244 # Because on each iteration it checks the "next two points"
1245 # of the actual. This way it doesn't go out of range
1246 if t
<= len(sp
.bezier_points
) - 3:
1247 p1
= sp
.bezier_points
[t
]
1248 p2
= sp
.bezier_points
[t
+ 1]
1249 p3
= sp
.bezier_points
[t
+ 2]
1251 vec_1
= p1
.co
- p2
.co
1252 vec_2
= p2
.co
- p3
.co
1254 if p2
.co
!= p1
.co
and p2
.co
!= p3
.co
:
1255 angle
= vec_1
.angle(vec_2
)
1256 angle_sum
+= degrees(angle
)
1257 # If sum of angles is grater than the limit
1258 if angle_sum
>= angle_limit
:
1259 p1
.select_control_point
= True
1260 p1
.select_left_handle
= True
1261 p1
.select_right_handle
= True
1263 p2
.select_control_point
= True
1264 p2
.select_left_handle
= True
1265 p2
.select_right_handle
= True
1267 p3
.select_control_point
= True
1268 p3
.select_left_handle
= True
1269 p3
.select_right_handle
= True
1273 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1274 bpy
.ops
.curve
.select_all(action
='INVERT')
1276 bpy
.ops
.curve
.delete(type='VERT')
1277 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1279 objects_to_delete
.append(ob_splines
)
1281 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1282 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1283 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1285 # Check if the strokes are a crosshatch
1286 if self
.is_crosshatch
:
1287 all_points_coords
= []
1288 for i
in range(len(ob_splines
.data
.splines
)):
1289 all_points_coords
.append([])
1291 all_points_coords
[i
] = [Vector((x
, y
, z
)) for
1292 x
, y
, z
in [bp
.co
for
1293 bp
in ob_splines
.data
.splines
[i
].bezier_points
]]
1295 all_intersections
= []
1296 checked_splines
= []
1297 for i
in range(len(all_points_coords
)):
1299 for t
in range(len(all_points_coords
[i
]) - 1):
1300 bp1_co
= all_points_coords
[i
][t
]
1301 bp2_co
= all_points_coords
[i
][t
+ 1]
1303 for i2
in range(len(all_points_coords
)):
1304 if i
!= i2
and i2
not in checked_splines
:
1305 for t2
in range(len(all_points_coords
[i2
]) - 1):
1306 bp3_co
= all_points_coords
[i2
][t2
]
1307 bp4_co
= all_points_coords
[i2
][t2
+ 1]
1309 intersec_coords
= intersect_line_line(
1310 bp1_co
, bp2_co
, bp3_co
, bp4_co
1312 if intersec_coords
is not None:
1313 dist
= (intersec_coords
[0] - intersec_coords
[1]).length
1315 if dist
<= self
.crosshatch_merge_distance
* 1.5:
1316 temp_co
, percent1
= intersect_point_line(
1317 intersec_coords
[0], bp1_co
, bp2_co
1319 if (percent1
>= -0.02 and percent1
<= 1.02):
1320 temp_co
, percent2
= intersect_point_line(
1321 intersec_coords
[1], bp3_co
, bp4_co
1323 if (percent2
>= -0.02 and percent2
<= 1.02):
1324 # Format: spline index, first point index from
1325 # corresponding segment, percentage from first point of
1326 # actual segment, coords of intersection point
1327 all_intersections
.append(
1329 ob_splines
.matrix_world
@ intersec_coords
[0])
1331 all_intersections
.append(
1333 ob_splines
.matrix_world
@ intersec_coords
[1])
1336 checked_splines
.append(i
)
1337 # Sort list by spline, then by corresponding first point index of segment,
1338 # and then by percentage from first point of segment: elements 0 and 1 respectively
1339 all_intersections
.sort(key
=operator
.itemgetter(0, 1, 2))
1341 self
.crosshatch_strokes_coords
= {}
1342 for i
in range(len(all_intersections
)):
1343 if not all_intersections
[i
][0] in self
.crosshatch_strokes_coords
:
1344 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]] = []
1346 self
.crosshatch_strokes_coords
[all_intersections
[i
][0]].append(
1347 all_intersections
[i
][3]
1348 ) # Save intersection coords
1350 self
.is_crosshatch
= False
1352 # Delete all duplicates
1353 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
1355 # If the main object has modifiers, turn their "viewport view status" to
1356 # what it was before the forced deactivation above
1357 if len(self
.main_object
.modifiers
) > 0:
1358 for m_idx
in range(len(self
.main_object
.modifiers
)):
1359 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1363 # Part of the Crosshatch process that is repeated when the operator is tweaked
1364 def crosshatch_surface_execute(self
):
1365 # If the main object uses modifiers deactivate them temporarily until the surface is joined
1366 # (without this the surface verts merging with the main object doesn't work well)
1367 self
.modifiers_prev_viewport_state
= []
1368 if len(self
.main_object
.modifiers
) > 0:
1369 for m_idx
in range(len(self
.main_object
.modifiers
)):
1370 self
.modifiers_prev_viewport_state
.append(self
.main_object
.modifiers
[m_idx
].show_viewport
)
1372 self
.main_object
.modifiers
[m_idx
].show_viewport
= False
1374 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1376 me_name
= "SURFSKIO_STK_TMP"
1377 me
= bpy
.data
.meshes
.new(me_name
)
1379 all_verts_coords
= []
1381 for st_idx
in self
.crosshatch_strokes_coords
:
1382 for co_idx
in range(len(self
.crosshatch_strokes_coords
[st_idx
])):
1383 coords
= self
.crosshatch_strokes_coords
[st_idx
][co_idx
]
1385 all_verts_coords
.append(coords
)
1388 all_edges
.append((len(all_verts_coords
) - 2, len(all_verts_coords
) - 1))
1390 me
.from_pydata(all_verts_coords
, all_edges
, [])
1394 ob
= bpy
.data
.objects
.new(me_name
, me
)
1396 bpy
.context
.collection
.objects
.link(ob
)
1398 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1400 bpy
.context
.view_layer
.objects
.active
= ob
1402 # Get together each vert and its nearest, to the middle position
1403 verts
= ob
.data
.vertices
1405 for i
in range(len(verts
)):
1406 shortest_dist
= None
1408 if i
not in checked_verts
:
1409 for t
in range(len(verts
)):
1410 if i
!= t
and t
not in checked_verts
:
1411 dist
= (verts
[i
].co
- verts
[t
].co
).length
1413 if shortest_dist
is not None:
1414 if dist
< shortest_dist
:
1415 shortest_dist
= dist
1418 shortest_dist
= dist
1421 middle_location
= (verts
[i
].co
+ verts
[nearest_vert
].co
) / 2
1423 verts
[i
].co
= middle_location
1424 verts
[nearest_vert
].co
= middle_location
1426 checked_verts
.append(i
)
1427 checked_verts
.append(nearest_vert
)
1429 # Calculate average length between all the generated edges
1430 ob
= bpy
.context
.object
1432 for ed
in ob
.data
.edges
:
1433 v1
= ob
.data
.vertices
[ed
.vertices
[0]]
1434 v2
= ob
.data
.vertices
[ed
.vertices
[1]]
1436 lengths_sum
+= (v1
.co
- v2
.co
).length
1438 edges_count
= len(ob
.data
.edges
)
1439 # possible division by zero here
1440 average_edge_length
= lengths_sum
/ edges_count
if edges_count
!= 0 else 0.0001
1442 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1443 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='SELECT')
1444 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN',
1445 threshold
=average_edge_length
/ 15.0)
1446 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1448 final_points_ob
= bpy
.context
.view_layer
.objects
.active
1450 # Make a dictionary with the verts related to each vert
1451 related_key_verts
= {}
1452 for ed
in final_points_ob
.data
.edges
:
1453 if not ed
.vertices
[0] in related_key_verts
:
1454 related_key_verts
[ed
.vertices
[0]] = []
1456 if not ed
.vertices
[1] in related_key_verts
:
1457 related_key_verts
[ed
.vertices
[1]] = []
1459 if not ed
.vertices
[1] in related_key_verts
[ed
.vertices
[0]]:
1460 related_key_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1462 if not ed
.vertices
[0] in related_key_verts
[ed
.vertices
[1]]:
1463 related_key_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1465 # Get groups of verts forming each face
1466 faces_verts_idx
= []
1467 for v1
in related_key_verts
: # verts-1 ....
1468 for v2
in related_key_verts
: # verts-2
1470 related_verts_in_common
= []
1471 v2_in_rel_v1
= False
1472 v1_in_rel_v2
= False
1473 for rel_v1
in related_key_verts
[v1
]:
1474 # Check if related verts of verts-1 are related verts of verts-2
1475 if rel_v1
in related_key_verts
[v2
]:
1476 related_verts_in_common
.append(rel_v1
)
1478 if v2
in related_key_verts
[v1
]:
1481 if v1
in related_key_verts
[v2
]:
1484 repeated_face
= False
1485 # If two verts have two related verts in common, they form a quad
1486 if len(related_verts_in_common
) == 2:
1487 # Check if the face is already saved
1488 for f_verts
in faces_verts_idx
:
1491 if len(f_verts
) == 4:
1496 if related_verts_in_common
[0] in f_verts
:
1498 if related_verts_in_common
[1] in f_verts
:
1501 if repeated_verts
== len(f_verts
):
1502 repeated_face
= True
1505 if not repeated_face
:
1506 faces_verts_idx
.append([v1
, related_verts_in_common
[0],
1507 v2
, related_verts_in_common
[1]])
1509 # If Two verts have one related vert in common and they are
1510 # related to each other, they form a triangle
1511 elif v2_in_rel_v1
and v1_in_rel_v2
and len(related_verts_in_common
) == 1:
1512 # Check if the face is already saved.
1513 for f_verts
in faces_verts_idx
:
1516 if len(f_verts
) == 3:
1521 if related_verts_in_common
[0] in f_verts
:
1524 if repeated_verts
== len(f_verts
):
1525 repeated_face
= True
1528 if not repeated_face
:
1529 faces_verts_idx
.append([v1
, related_verts_in_common
[0], v2
])
1531 # Keep only the faces that don't overlap by ignoring
1532 # quads that overlap with two adjacent triangles
1533 faces_to_not_include_idx
= [] # Indices of faces_verts_idx to eliminate
1534 for i
in range(len(faces_verts_idx
)):
1535 for t
in range(len(faces_verts_idx
)):
1539 if len(faces_verts_idx
[i
]) == 4 and len(faces_verts_idx
[t
]) == 3:
1540 for v_idx
in faces_verts_idx
[t
]:
1541 if v_idx
in faces_verts_idx
[i
]:
1542 verts_in_common
+= 1
1543 # If it doesn't have all it's vertices repeated in the other face
1544 if verts_in_common
== 3:
1545 if i
not in faces_to_not_include_idx
:
1546 faces_to_not_include_idx
.append(i
)
1549 all_surface_verts_co
= []
1550 verts_idx_translation
= {}
1551 for i
in range(len(final_points_ob
.data
.vertices
)):
1552 coords
= final_points_ob
.data
.vertices
[i
].co
1553 all_surface_verts_co
.append([coords
[0], coords
[1], coords
[2]])
1555 # Verts of each face.
1556 all_surface_faces
= []
1557 for i
in range(len(faces_verts_idx
)):
1558 if i
not in faces_to_not_include_idx
:
1560 for v_idx
in faces_verts_idx
[i
]:
1563 all_surface_faces
.append(face
)
1566 surf_me_name
= "SURFSKIO_surface"
1567 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
1569 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
1573 ob_surface
= bpy
.data
.objects
.new(surf_me_name
, me_surf
)
1574 bpy
.context
.collection
.objects
.link(ob_surface
)
1576 # Delete final points temporal object
1577 bpy
.ops
.object.delete({"selected_objects": [final_points_ob
]})
1579 # Delete isolated verts if there are any
1580 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1581 ob_surface
.select_set(True)
1582 bpy
.context
.view_layer
.objects
.active
= ob_surface
1584 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1585 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1586 bpy
.ops
.mesh
.select_face_by_sides(type='NOTEQUAL')
1587 bpy
.ops
.mesh
.delete()
1588 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1590 # Join crosshatch results with original mesh
1592 # Calculate a distance to merge the verts of the crosshatch surface to the main object
1593 edges_length_sum
= 0
1594 for ed
in ob_surface
.data
.edges
:
1595 edges_length_sum
+= (
1596 ob_surface
.data
.vertices
[ed
.vertices
[0]].co
-
1597 ob_surface
.data
.vertices
[ed
.vertices
[1]].co
1600 if len(ob_surface
.data
.edges
) > 0:
1601 average_surface_edges_length
= edges_length_sum
/ len(ob_surface
.data
.edges
)
1603 average_surface_edges_length
= 0.0001
1605 # Make dictionary with all the verts connected to each vert, on the new surface object.
1606 surface_connected_verts
= {}
1607 for ed
in ob_surface
.data
.edges
:
1608 if not ed
.vertices
[0] in surface_connected_verts
:
1609 surface_connected_verts
[ed
.vertices
[0]] = []
1611 surface_connected_verts
[ed
.vertices
[0]].append(ed
.vertices
[1])
1613 if ed
.vertices
[1] not in surface_connected_verts
:
1614 surface_connected_verts
[ed
.vertices
[1]] = []
1616 surface_connected_verts
[ed
.vertices
[1]].append(ed
.vertices
[0])
1618 # Duplicate the new surface object, and use shrinkwrap to
1619 # calculate later the nearest verts to the main object
1620 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1621 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1622 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1624 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
1626 final_ob_duplicate
= bpy
.context
.view_layer
.objects
.active
1628 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
1629 shrinkwrap_modifier
= final_ob_duplicate
.modifiers
[-1]
1630 shrinkwrap_modifier
.wrap_method
= "NEAREST_VERTEX"
1631 shrinkwrap_modifier
.target
= self
.main_object
1633 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', apply_as
='DATA', modifier
=shrinkwrap_modifier
.name
)
1635 # Make list with verts of original mesh as index and coords as value
1636 main_object_verts_coords
= []
1637 for v
in self
.main_object
.data
.vertices
:
1638 coords
= self
.main_object
.matrix_world
@ v
.co
1640 # To avoid problems when taking "-0.00" as a different value as "0.00"
1641 for c
in range(len(coords
)):
1642 if "%.3f" % coords
[c
] == "-0.00":
1645 main_object_verts_coords
.append(["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]])
1647 tuple(main_object_verts_coords
)
1649 # Determine which verts will be merged, snap them to the nearest verts
1650 # on the original verts, and get them selected
1651 crosshatch_verts_to_merge
= []
1652 if self
.automatic_join
:
1653 for i
in range(len(ob_surface
.data
.vertices
)):
1654 # Calculate the distance from each of the connected verts to the actual vert,
1655 # and compare it with the distance they would have if joined.
1656 # If they don't change much, that vert can be joined
1657 merge_actual_vert
= True
1658 if len(surface_connected_verts
[i
]) < 4:
1659 for c_v_idx
in surface_connected_verts
[i
]:
1660 points_original
= []
1661 points_original
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1662 points_original
.append(ob_surface
.data
.vertices
[i
].co
)
1665 points_target
.append(ob_surface
.data
.vertices
[c_v_idx
].co
)
1666 points_target
.append(final_ob_duplicate
.data
.vertices
[i
].co
)
1668 vec_A
= points_original
[0] - points_original
[1]
1669 vec_B
= points_target
[0] - points_target
[1]
1671 dist_A
= (points_original
[0] - points_original
[1]).length
1672 dist_B
= (points_target
[0] - points_target
[1]).length
1675 points_original
[0] == points_original
[1] or
1676 points_target
[0] == points_target
[1]
1677 ): # If any vector's length is zero
1679 angle
= vec_A
.angle(vec_B
) / pi
1683 # Set a range of acceptable variation in the connected edges
1684 if dist_B
> dist_A
* 1.7 * self
.join_stretch_factor
or \
1685 dist_B
< dist_A
/ 2 / self
.join_stretch_factor
or \
1686 angle
>= 0.15 * self
.join_stretch_factor
:
1688 merge_actual_vert
= False
1691 merge_actual_vert
= False
1693 if merge_actual_vert
:
1694 coords
= final_ob_duplicate
.data
.vertices
[i
].co
1695 # To avoid problems when taking "-0.000" as a different value as "0.00"
1696 for c
in range(len(coords
)):
1697 if "%.3f" % coords
[c
] == "-0.00":
1700 comparison_coords
= ["%.3f" % coords
[0], "%.3f" % coords
[1], "%.3f" % coords
[2]]
1702 if comparison_coords
in main_object_verts_coords
:
1703 # Get the index of the vert with those coords in the main object
1704 main_object_related_vert_idx
= main_object_verts_coords
.index(comparison_coords
)
1706 if self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select
is True or \
1707 self
.main_object_selected_verts_count
== 0:
1709 ob_surface
.data
.vertices
[i
].co
= final_ob_duplicate
.data
.vertices
[i
].co
1710 ob_surface
.data
.vertices
[i
].select_set(True)
1711 crosshatch_verts_to_merge
.append(i
)
1713 # Make sure the vert in the main object is selected,
1714 # in case it wasn't selected and the "join crosshatch" option is active
1715 self
.main_object
.data
.vertices
[main_object_related_vert_idx
].select_set(True)
1717 # Delete duplicated object
1718 bpy
.ops
.object.delete({"selected_objects": [final_ob_duplicate
]})
1720 # Join crosshatched surface and main object
1721 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1722 ob_surface
.select_set(True)
1723 self
.main_object
.select_set(True)
1724 bpy
.context
.view_layer
.objects
.active
= self
.main_object
1726 bpy
.ops
.object.join('INVOKE_REGION_WIN')
1728 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1729 # Perform Remove doubles to merge verts
1730 if not (self
.automatic_join
is False and self
.main_object_selected_verts_count
== 0):
1731 bpy
.ops
.mesh
.remove_doubles(threshold
=0.0001)
1733 bpy
.ops
.mesh
.select_all(action
='DESELECT')
1735 # If the main object has modifiers, turn their "viewport view status"
1736 # to what it was before the forced deactivation above
1737 if len(self
.main_object
.modifiers
) > 0:
1738 for m_idx
in range(len(self
.main_object
.modifiers
)):
1739 self
.main_object
.modifiers
[m_idx
].show_viewport
= self
.modifiers_prev_viewport_state
[m_idx
]
1743 def rectangular_surface(self
):
1745 all_selected_edges_idx
= []
1746 all_selected_verts
= []
1748 for ed
in self
.main_object
.data
.edges
:
1750 all_selected_edges_idx
.append(ed
.index
)
1753 if not ed
.vertices
[0] in all_selected_verts
:
1754 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[0]])
1755 if not ed
.vertices
[1] in all_selected_verts
:
1756 all_selected_verts
.append(self
.main_object
.data
.vertices
[ed
.vertices
[1]])
1758 # All verts (both from each edge) to determine later
1759 # which are at the tips (those not repeated twice)
1760 all_verts_idx
.append(ed
.vertices
[0])
1761 all_verts_idx
.append(ed
.vertices
[1])
1763 # Identify the tips and "middle-vertex" that separates U from V, if there is one
1764 all_chains_tips_idx
= []
1765 for v_idx
in all_verts_idx
:
1766 if all_verts_idx
.count(v_idx
) < 2:
1767 all_chains_tips_idx
.append(v_idx
)
1769 edges_connected_to_tips
= []
1770 for ed
in self
.main_object
.data
.edges
:
1771 if (ed
.vertices
[0] in all_chains_tips_idx
or ed
.vertices
[1] in all_chains_tips_idx
) and \
1772 not (ed
.vertices
[0] in all_verts_idx
and ed
.vertices
[1] in all_verts_idx
):
1774 edges_connected_to_tips
.append(ed
)
1776 # Check closed selections
1777 # List with groups of three verts, where the first element of the pair is
1778 # the unselected vert of a closed selection and the other two elements are the
1779 # selected neighbor verts (it will be useful to determine which selection chain
1780 # the unselected vert belongs to, and determine the "middle-vertex")
1781 single_unselected_verts_and_neighbors
= []
1783 # To identify a "closed" selection (a selection that is a closed chain except
1784 # for one vertex) find the vertex in common that have the edges connected to tips.
1785 # If there is a vertex in common, that one is the unselected vert that closes
1786 # the selection or is a "middle-vertex"
1787 single_unselected_verts
= []
1788 for ed
in edges_connected_to_tips
:
1789 for ed_b
in edges_connected_to_tips
:
1791 if ed
.vertices
[0] == ed_b
.vertices
[0] and \
1792 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1793 ed
.vertices
[0] not in single_unselected_verts
:
1795 # The second element is one of the tips of the selected
1796 # vertices of the closed selection
1797 single_unselected_verts_and_neighbors
.append(
1798 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[1]]
1800 single_unselected_verts
.append(ed
.vertices
[0])
1802 elif ed
.vertices
[0] == ed_b
.vertices
[1] and \
1803 not self
.main_object
.data
.vertices
[ed
.vertices
[0]].select
and \
1804 ed
.vertices
[0] not in single_unselected_verts
:
1806 single_unselected_verts_and_neighbors
.append(
1807 [ed
.vertices
[0], ed
.vertices
[1], ed_b
.vertices
[0]]
1809 single_unselected_verts
.append(ed
.vertices
[0])
1811 elif ed
.vertices
[1] == ed_b
.vertices
[0] and \
1812 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1813 ed
.vertices
[1] not in single_unselected_verts
:
1815 single_unselected_verts_and_neighbors
.append(
1816 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[1]]
1818 single_unselected_verts
.append(ed
.vertices
[1])
1820 elif ed
.vertices
[1] == ed_b
.vertices
[1] and \
1821 not self
.main_object
.data
.vertices
[ed
.vertices
[1]].select
and \
1822 ed
.vertices
[1] not in single_unselected_verts
:
1824 single_unselected_verts_and_neighbors
.append(
1825 [ed
.vertices
[1], ed
.vertices
[0], ed_b
.vertices
[0]]
1827 single_unselected_verts
.append(ed
.vertices
[1])
1830 middle_vertex_idx
= None
1831 tips_to_discard_idx
= []
1833 # Check if there is a "middle-vertex", and get its index
1834 for i
in range(0, len(single_unselected_verts_and_neighbors
)):
1835 actual_chain_verts
= self
.get_ordered_verts(
1836 self
.main_object
, all_selected_edges_idx
,
1837 all_verts_idx
, single_unselected_verts_and_neighbors
[i
][1],
1841 if single_unselected_verts_and_neighbors
[i
][2] != \
1842 actual_chain_verts
[len(actual_chain_verts
) - 1].index
:
1844 middle_vertex_idx
= single_unselected_verts_and_neighbors
[i
][0]
1845 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][1])
1846 tips_to_discard_idx
.append(single_unselected_verts_and_neighbors
[i
][2])
1848 # List with pairs of verts that belong to the tips of each selection chain (row)
1849 verts_tips_same_chain_idx
= []
1850 if len(all_chains_tips_idx
) >= 2:
1852 for i
in range(0, len(all_chains_tips_idx
)):
1853 if all_chains_tips_idx
[i
] not in checked_v
:
1854 v_chain
= self
.get_ordered_verts(
1855 self
.main_object
, all_selected_edges_idx
,
1856 all_verts_idx
, all_chains_tips_idx
[i
],
1857 middle_vertex_idx
, None
1860 verts_tips_same_chain_idx
.append([v_chain
[0].index
, v_chain
[len(v_chain
) - 1].index
])
1862 checked_v
.append(v_chain
[0].index
)
1863 checked_v
.append(v_chain
[len(v_chain
) - 1].index
)
1865 # Selection tips (vertices).
1866 verts_tips_parsed_idx
= []
1867 if len(all_chains_tips_idx
) >= 2:
1868 for spec_v_idx
in all_chains_tips_idx
:
1869 if (spec_v_idx
not in tips_to_discard_idx
):
1870 verts_tips_parsed_idx
.append(spec_v_idx
)
1872 # Identify the type of selection made by the user
1873 if middle_vertex_idx
is not None:
1874 # If there are 4 tips (two selection chains), and
1875 # there is only one single unselected vert (the middle vert)
1876 if len(all_chains_tips_idx
) == 4 and len(single_unselected_verts_and_neighbors
) == 1:
1877 selection_type
= "TWO_CONNECTED"
1879 # The type of the selection was not identified, the script stops.
1880 self
.report({'WARNING'}, "The selection isn't valid.")
1881 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1882 self
.cleanup_on_interruption()
1883 self
.stopping_errors
= True
1887 if len(all_chains_tips_idx
) == 2: # If there are 2 tips
1888 selection_type
= "SINGLE"
1889 elif len(all_chains_tips_idx
) == 4: # If there are 4 tips
1890 selection_type
= "TWO_NOT_CONNECTED"
1891 elif len(all_chains_tips_idx
) == 0:
1892 if len(self
.main_splines
.data
.splines
) > 1:
1893 selection_type
= "NO_SELECTION"
1895 # If the selection was not identified and there is only one stroke,
1896 # there's no possibility to build a surface, so the script is interrupted
1897 self
.report({'WARNING'}, "The selection isn't valid.")
1898 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1899 self
.cleanup_on_interruption()
1900 self
.stopping_errors
= True
1904 # The type of the selection was not identified, the script stops
1905 self
.report({'WARNING'}, "The selection isn't valid.")
1907 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1908 self
.cleanup_on_interruption()
1910 self
.stopping_errors
= True
1914 # If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
1915 if selection_type
== "TWO_NOT_CONNECTED" and len(self
.main_splines
.data
.splines
) == 1:
1916 self
.report({'WARNING'},
1917 "At least two strokes are needed when there are two not connected selections")
1918 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1919 self
.cleanup_on_interruption()
1920 self
.stopping_errors
= True
1924 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1926 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
1927 self
.main_splines
.select_set(True)
1928 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
1930 # Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
1931 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1932 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1933 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1934 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1935 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1936 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1937 bpy
.ops
.curve
.smooth('INVOKE_REGION_WIN')
1938 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
1940 self
.selection_U_exists
= False
1941 self
.selection_U2_exists
= False
1942 self
.selection_V_exists
= False
1943 self
.selection_V2_exists
= False
1945 self
.selection_U_is_closed
= False
1946 self
.selection_U2_is_closed
= False
1947 self
.selection_V_is_closed
= False
1948 self
.selection_V2_is_closed
= False
1950 # Define what vertices are at the tips of each selection and are not the middle-vertex
1951 if selection_type
== "TWO_CONNECTED":
1952 self
.selection_U_exists
= True
1953 self
.selection_V_exists
= True
1955 closing_vert_U_idx
= None
1956 closing_vert_V_idx
= None
1957 closing_vert_U2_idx
= None
1958 closing_vert_V2_idx
= None
1960 # Determine which selection is Selection-U and which is Selection-V
1963 points_first_stroke_tips
= []
1966 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[0]].co
1969 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
1972 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[verts_tips_parsed_idx
[1]].co
1975 self
.main_object
.matrix_world
@ self
.main_object
.data
.vertices
[middle_vertex_idx
].co
1977 points_first_stroke_tips
.append(
1978 self
.main_splines
.data
.splines
[0].bezier_points
[0].co
1980 points_first_stroke_tips
.append(
1981 self
.main_splines
.data
.splines
[0].bezier_points
[
1982 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
1986 angle_A
= self
.orientation_difference(points_A
, points_first_stroke_tips
)
1987 angle_B
= self
.orientation_difference(points_B
, points_first_stroke_tips
)
1989 if angle_A
< angle_B
:
1990 first_vert_U_idx
= verts_tips_parsed_idx
[0]
1991 first_vert_V_idx
= verts_tips_parsed_idx
[1]
1993 first_vert_U_idx
= verts_tips_parsed_idx
[1]
1994 first_vert_V_idx
= verts_tips_parsed_idx
[0]
1996 elif selection_type
== "SINGLE" or selection_type
== "TWO_NOT_CONNECTED":
1997 first_sketched_point_first_stroke_co
= self
.main_splines
.data
.splines
[0].bezier_points
[0].co
1998 last_sketched_point_first_stroke_co
= \
1999 self
.main_splines
.data
.splines
[0].bezier_points
[
2000 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2002 first_sketched_point_last_stroke_co
= \
2003 self
.main_splines
.data
.splines
[
2004 len(self
.main_splines
.data
.splines
) - 1
2005 ].bezier_points
[0].co
2006 if len(self
.main_splines
.data
.splines
) > 1:
2007 first_sketched_point_second_stroke_co
= self
.main_splines
.data
.splines
[1].bezier_points
[0].co
2008 last_sketched_point_second_stroke_co
= \
2009 self
.main_splines
.data
.splines
[1].bezier_points
[
2010 len(self
.main_splines
.data
.splines
[1].bezier_points
) - 1
2013 single_unselected_neighbors
= [] # Only the neighbors of the single unselected verts
2014 for verts_neig_idx
in single_unselected_verts_and_neighbors
:
2015 single_unselected_neighbors
.append(verts_neig_idx
[1])
2016 single_unselected_neighbors
.append(verts_neig_idx
[2])
2018 all_chains_tips_and_middle_vert
= []
2019 for v_idx
in all_chains_tips_idx
:
2020 if v_idx
not in single_unselected_neighbors
:
2021 all_chains_tips_and_middle_vert
.append(v_idx
)
2023 all_chains_tips_and_middle_vert
+= single_unselected_verts
2025 all_participating_verts
= all_chains_tips_and_middle_vert
+ all_verts_idx
2027 # The tip of the selected vertices nearest to the first point of the first sketched stroke
2028 nearest_tip_to_first_st_first_pt_idx
, shortest_distance_to_first_stroke
= \
2029 self
.shortest_distance(
2031 first_sketched_point_first_stroke_co
,
2032 all_chains_tips_and_middle_vert
2034 # If the nearest tip is not from a closed selection, get the opposite tip vertex index
2035 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2036 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2038 nearest_tip_to_first_st_first_pt_opposite_idx
= \
2040 nearest_tip_to_first_st_first_pt_idx
,
2041 verts_tips_same_chain_idx
2043 # The tip of the selected vertices nearest to the last point of the first sketched stroke
2044 nearest_tip_to_first_st_last_pt_idx
, temp_dist
= \
2045 self
.shortest_distance(
2047 last_sketched_point_first_stroke_co
,
2048 all_chains_tips_and_middle_vert
2050 # The tip of the selected vertices nearest to the first point of the last sketched stroke
2051 nearest_tip_to_last_st_first_pt_idx
, shortest_distance_to_last_stroke
= \
2052 self
.shortest_distance(
2054 first_sketched_point_last_stroke_co
,
2055 all_chains_tips_and_middle_vert
2057 if len(self
.main_splines
.data
.splines
) > 1:
2058 # The selected vertex nearest to the first point of the second sketched stroke
2059 # (This will be useful to determine the direction of the closed
2060 # selection V when extruding along strokes)
2061 nearest_vert_to_second_st_first_pt_idx
, temp_dist
= \
2062 self
.shortest_distance(
2064 first_sketched_point_second_stroke_co
,
2067 # The selected vertex nearest to the first point of the second sketched stroke
2068 # (This will be useful to determine the direction of the closed
2069 # selection V2 when extruding along strokes)
2070 nearest_vert_to_second_st_last_pt_idx
, temp_dist
= \
2071 self
.shortest_distance(
2073 last_sketched_point_second_stroke_co
,
2076 # Determine if the single selection will be treated as U or as V
2078 for i
in all_selected_edges_idx
:
2080 (self
.main_object
.matrix_world
@
2081 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[0]].co
) -
2082 (self
.main_object
.matrix_world
@
2083 self
.main_object
.data
.vertices
[self
.main_object
.data
.edges
[i
].vertices
[1]].co
)
2086 average_edge_length
= edges_sum
/ len(all_selected_edges_idx
)
2088 # Get shortest distance from the first point of the last stroke to any participating vertex
2089 temp_idx
, shortest_distance_to_last_stroke
= \
2090 self
.shortest_distance(
2092 first_sketched_point_last_stroke_co
,
2093 all_participating_verts
2095 # If the beginning of the first stroke is near enough, and its orientation
2096 # difference with the first edge of the nearest selection chain is not too high,
2097 # interpret things as an "extrude along strokes" instead of "extrude through strokes"
2098 if shortest_distance_to_first_stroke
< average_edge_length
/ 4 and \
2099 shortest_distance_to_last_stroke
< average_edge_length
and \
2100 len(self
.main_splines
.data
.splines
) > 1:
2102 self
.selection_U_exists
= False
2103 self
.selection_V_exists
= True
2104 # If the first selection is not closed
2105 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2106 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2107 self
.selection_V_is_closed
= False
2108 first_neighbor_V_idx
= None
2109 closing_vert_U_idx
= None
2110 closing_vert_U2_idx
= None
2111 closing_vert_V_idx
= None
2112 closing_vert_V2_idx
= None
2114 first_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2116 if selection_type
== "TWO_NOT_CONNECTED":
2117 self
.selection_V2_exists
= True
2119 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2121 self
.selection_V_is_closed
= True
2122 closing_vert_V_idx
= nearest_tip_to_first_st_first_pt_idx
2124 # Get the neighbors of the first (unselected) vert of the closed selection U.
2126 for verts
in single_unselected_verts_and_neighbors
:
2127 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2128 vert_neighbors
.append(verts
[1])
2129 vert_neighbors
.append(verts
[2])
2132 verts_V
= self
.get_ordered_verts(
2133 self
.main_object
, all_selected_edges_idx
,
2134 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2137 for i
in range(0, len(verts_V
)):
2138 if verts_V
[i
].index
== nearest_vert_to_second_st_first_pt_idx
:
2139 # If the vertex nearest to the first point of the second stroke
2140 # is in the first half of the selected verts
2141 if i
>= len(verts_V
) / 2:
2142 first_vert_V_idx
= vert_neighbors
[1]
2145 first_vert_V_idx
= vert_neighbors
[0]
2148 if selection_type
== "TWO_NOT_CONNECTED":
2149 self
.selection_V2_exists
= True
2150 # If the second selection is not closed
2151 if nearest_tip_to_first_st_last_pt_idx
not in single_unselected_verts
or \
2152 nearest_tip_to_first_st_last_pt_idx
== middle_vertex_idx
:
2154 self
.selection_V2_is_closed
= False
2155 first_neighbor_V2_idx
= None
2156 closing_vert_V2_idx
= None
2157 first_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2160 self
.selection_V2_is_closed
= True
2161 closing_vert_V2_idx
= nearest_tip_to_first_st_last_pt_idx
2163 # Get the neighbors of the first (unselected) vert of the closed selection U
2165 for verts
in single_unselected_verts_and_neighbors
:
2166 if verts
[0] == nearest_tip_to_first_st_last_pt_idx
:
2167 vert_neighbors
.append(verts
[1])
2168 vert_neighbors
.append(verts
[2])
2171 verts_V2
= self
.get_ordered_verts(
2172 self
.main_object
, all_selected_edges_idx
,
2173 all_verts_idx
, vert_neighbors
[0], middle_vertex_idx
, None
2176 for i
in range(0, len(verts_V2
)):
2177 if verts_V2
[i
].index
== nearest_vert_to_second_st_last_pt_idx
:
2178 # If the vertex nearest to the first point of the second stroke
2179 # is in the first half of the selected verts
2180 if i
>= len(verts_V2
) / 2:
2181 first_vert_V2_idx
= vert_neighbors
[1]
2184 first_vert_V2_idx
= vert_neighbors
[0]
2187 self
.selection_V2_exists
= False
2190 self
.selection_U_exists
= True
2191 self
.selection_V_exists
= False
2192 # If the first selection is not closed
2193 if nearest_tip_to_first_st_first_pt_idx
not in single_unselected_verts
or \
2194 nearest_tip_to_first_st_first_pt_idx
== middle_vertex_idx
:
2195 self
.selection_U_is_closed
= False
2196 first_neighbor_U_idx
= None
2197 closing_vert_U_idx
= None
2201 self
.main_object
.matrix_world
@
2202 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2205 self
.main_object
.matrix_world
@
2206 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_opposite_idx
].co
2208 points_first_stroke_tips
= []
2209 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2210 points_first_stroke_tips
.append(
2211 self
.main_splines
.data
.splines
[0].bezier_points
[
2212 len(self
.main_splines
.data
.splines
[0].bezier_points
) - 1
2215 vec_A
= points_tips
[0] - points_tips
[1]
2216 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2218 # Compare the direction of the selection and the first
2219 # grease pencil stroke to determine which is the "first" vertex of the selection
2220 if vec_A
.dot(vec_B
) < 0:
2221 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_opposite_idx
2223 first_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2226 self
.selection_U_is_closed
= True
2227 closing_vert_U_idx
= nearest_tip_to_first_st_first_pt_idx
2229 # Get the neighbors of the first (unselected) vert of the closed selection U
2231 for verts
in single_unselected_verts_and_neighbors
:
2232 if verts
[0] == nearest_tip_to_first_st_first_pt_idx
:
2233 vert_neighbors
.append(verts
[1])
2234 vert_neighbors
.append(verts
[2])
2237 points_first_and_neighbor
= []
2238 points_first_and_neighbor
.append(
2239 self
.main_object
.matrix_world
@
2240 self
.main_object
.data
.vertices
[nearest_tip_to_first_st_first_pt_idx
].co
2242 points_first_and_neighbor
.append(
2243 self
.main_object
.matrix_world
@
2244 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2246 points_first_stroke_tips
= []
2247 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[0].co
)
2248 points_first_stroke_tips
.append(self
.main_splines
.data
.splines
[0].bezier_points
[1].co
)
2250 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2251 vec_B
= points_first_stroke_tips
[0] - points_first_stroke_tips
[1]
2253 # Compare the direction of the selection and the first grease pencil stroke to
2254 # determine which is the vertex neighbor to the first vertex (unselected) of
2255 # the closed selection. This will determine the direction of the closed selection
2256 if vec_A
.dot(vec_B
) < 0:
2257 first_vert_U_idx
= vert_neighbors
[1]
2259 first_vert_U_idx
= vert_neighbors
[0]
2261 if selection_type
== "TWO_NOT_CONNECTED":
2262 self
.selection_U2_exists
= True
2263 # If the second selection is not closed
2264 if nearest_tip_to_last_st_first_pt_idx
not in single_unselected_verts
or \
2265 nearest_tip_to_last_st_first_pt_idx
== middle_vertex_idx
:
2267 self
.selection_U2_is_closed
= False
2268 first_neighbor_U2_idx
= None
2269 closing_vert_U2_idx
= None
2270 first_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2272 self
.selection_U2_is_closed
= True
2273 closing_vert_U2_idx
= nearest_tip_to_last_st_first_pt_idx
2275 # Get the neighbors of the first (unselected) vert of the closed selection U
2277 for verts
in single_unselected_verts_and_neighbors
:
2278 if verts
[0] == nearest_tip_to_last_st_first_pt_idx
:
2279 vert_neighbors
.append(verts
[1])
2280 vert_neighbors
.append(verts
[2])
2283 points_first_and_neighbor
= []
2284 points_first_and_neighbor
.append(
2285 self
.main_object
.matrix_world
@
2286 self
.main_object
.data
.vertices
[nearest_tip_to_last_st_first_pt_idx
].co
2288 points_first_and_neighbor
.append(
2289 self
.main_object
.matrix_world
@
2290 self
.main_object
.data
.vertices
[vert_neighbors
[0]].co
2292 points_last_stroke_tips
= []
2293 points_last_stroke_tips
.append(
2294 self
.main_splines
.data
.splines
[
2295 len(self
.main_splines
.data
.splines
) - 1
2296 ].bezier_points
[0].co
2298 points_last_stroke_tips
.append(
2299 self
.main_splines
.data
.splines
[
2300 len(self
.main_splines
.data
.splines
) - 1
2301 ].bezier_points
[1].co
2303 vec_A
= points_first_and_neighbor
[0] - points_first_and_neighbor
[1]
2304 vec_B
= points_last_stroke_tips
[0] - points_last_stroke_tips
[1]
2306 # Compare the direction of the selection and the last grease pencil stroke to
2307 # determine which is the vertex neighbor to the first vertex (unselected) of
2308 # the closed selection. This will determine the direction of the closed selection
2309 if vec_A
.dot(vec_B
) < 0:
2310 first_vert_U2_idx
= vert_neighbors
[1]
2312 first_vert_U2_idx
= vert_neighbors
[0]
2314 self
.selection_U2_exists
= False
2316 elif selection_type
== "NO_SELECTION":
2317 self
.selection_U_exists
= False
2318 self
.selection_V_exists
= False
2320 # Get an ordered list of the vertices of Selection-U
2321 verts_ordered_U
= []
2322 if self
.selection_U_exists
:
2323 verts_ordered_U
= self
.get_ordered_verts(
2324 self
.main_object
, all_selected_edges_idx
,
2325 all_verts_idx
, first_vert_U_idx
,
2326 middle_vertex_idx
, closing_vert_U_idx
2328 verts_ordered_U_indices
= [x
.index
for x
in verts_ordered_U
]
2330 # Get an ordered list of the vertices of Selection-U2
2331 verts_ordered_U2
= []
2332 if self
.selection_U2_exists
:
2333 verts_ordered_U2
= self
.get_ordered_verts(
2334 self
.main_object
, all_selected_edges_idx
,
2335 all_verts_idx
, first_vert_U2_idx
,
2336 middle_vertex_idx
, closing_vert_U2_idx
2338 verts_ordered_U2_indices
= [x
.index
for x
in verts_ordered_U2
]
2340 # Get an ordered list of the vertices of Selection-V
2341 verts_ordered_V
= []
2342 if self
.selection_V_exists
:
2343 verts_ordered_V
= self
.get_ordered_verts(
2344 self
.main_object
, all_selected_edges_idx
,
2345 all_verts_idx
, first_vert_V_idx
,
2346 middle_vertex_idx
, closing_vert_V_idx
2348 verts_ordered_V_indices
= [x
.index
for x
in verts_ordered_V
]
2350 # Get an ordered list of the vertices of Selection-V2
2351 verts_ordered_V2
= []
2352 if self
.selection_V2_exists
:
2353 verts_ordered_V2
= self
.get_ordered_verts(
2354 self
.main_object
, all_selected_edges_idx
,
2355 all_verts_idx
, first_vert_V2_idx
,
2356 middle_vertex_idx
, closing_vert_V2_idx
2358 verts_ordered_V2_indices
= [x
.index
for x
in verts_ordered_V2
]
2360 # Check if when there are two-not-connected selections both have the same
2361 # number of verts. If not terminate the script
2362 if ((self
.selection_U2_exists
and len(verts_ordered_U
) != len(verts_ordered_U2
)) or
2363 (self
.selection_V2_exists
and len(verts_ordered_V
) != len(verts_ordered_V2
))):
2365 self
.report({'WARNING'}, "Both selections must have the same number of edges")
2367 self
.cleanup_on_interruption()
2368 self
.stopping_errors
= True
2372 # Calculate edges U proportions
2373 # Sum selected edges U lengths
2374 edges_lengths_U
= []
2375 edges_lengths_sum_U
= 0
2377 if self
.selection_U_exists
:
2378 edges_lengths_U
, edges_lengths_sum_U
= self
.get_chain_length(
2382 if self
.selection_U2_exists
:
2383 edges_lengths_U2
, edges_lengths_sum_U2
= self
.get_chain_length(
2387 # Sum selected edges V lengths
2388 edges_lengths_V
= []
2389 edges_lengths_sum_V
= 0
2391 if self
.selection_V_exists
:
2392 edges_lengths_V
, edges_lengths_sum_V
= self
.get_chain_length(
2396 if self
.selection_V2_exists
:
2397 edges_lengths_V2
, edges_lengths_sum_V2
= self
.get_chain_length(
2402 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2403 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN',
2404 number_cuts
=bpy
.context
.scene
.bsurfaces
.SURFSK_precision
)
2405 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2408 edges_proportions_U
= []
2409 edges_proportions_U
= self
.get_edges_proportions(
2410 edges_lengths_U
, edges_lengths_sum_U
,
2411 self
.selection_U_exists
, self
.edges_U
2413 verts_count_U
= len(edges_proportions_U
) + 1
2415 if self
.selection_U2_exists
:
2416 edges_proportions_U2
= []
2417 edges_proportions_U2
= self
.get_edges_proportions(
2418 edges_lengths_U2
, edges_lengths_sum_U2
,
2419 self
.selection_U2_exists
, self
.edges_V
2421 verts_count_U2
= len(edges_proportions_U2
) + 1
2424 edges_proportions_V
= []
2425 edges_proportions_V
= self
.get_edges_proportions(
2426 edges_lengths_V
, edges_lengths_sum_V
,
2427 self
.selection_V_exists
, self
.edges_V
2429 verts_count_V
= len(edges_proportions_V
) + 1
2431 if self
.selection_V2_exists
:
2432 edges_proportions_V2
= []
2433 edges_proportions_V2
= self
.get_edges_proportions(
2434 edges_lengths_V2
, edges_lengths_sum_V2
,
2435 self
.selection_V2_exists
, self
.edges_V
2437 verts_count_V2
= len(edges_proportions_V2
) + 1
2439 # Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
2440 # the actual sketched curves with a "closing segment"
2441 if self
.cyclic_follow
and not self
.selection_V_exists
and not \
2442 ((self
.selection_U_exists
and not self
.selection_U_is_closed
) or
2443 (self
.selection_U2_exists
and not self
.selection_U2_is_closed
)):
2445 simplified_spline_coords
= []
2446 simplified_curve
= []
2447 ob_simplified_curve
= []
2448 splines_first_v_co
= []
2449 for i
in range(len(self
.main_splines
.data
.splines
)):
2450 # Create a curve object for the actual spline "cyclic extension"
2451 simplified_curve
.append(bpy
.data
.curves
.new('SURFSKIO_simpl_crv', 'CURVE'))
2452 ob_simplified_curve
.append(bpy
.data
.objects
.new('SURFSKIO_simpl_crv', simplified_curve
[i
]))
2453 bpy
.context
.collection
.objects
.link(ob_simplified_curve
[i
])
2455 simplified_curve
[i
].dimensions
= "3D"
2458 for bp
in self
.main_splines
.data
.splines
[i
].bezier_points
:
2459 spline_coords
.append(bp
.co
)
2462 simplified_spline_coords
.append(self
.simplify_spline(spline_coords
, 5))
2464 # Get the coordinates of the first vert of the actual spline
2465 splines_first_v_co
.append(simplified_spline_coords
[i
][0])
2467 # Generate the spline
2468 spline
= simplified_curve
[i
].splines
.new('BEZIER')
2469 # less one because one point is added when the spline is created
2470 spline
.bezier_points
.add(len(simplified_spline_coords
[i
]) - 1)
2471 for p
in range(0, len(simplified_spline_coords
[i
])):
2472 spline
.bezier_points
[p
].co
= simplified_spline_coords
[i
][p
]
2474 spline
.use_cyclic_u
= True
2476 spline_bp_count
= len(spline
.bezier_points
)
2478 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2479 ob_simplified_curve
[i
].select_set(True)
2480 bpy
.context
.view_layer
.objects
.active
= ob_simplified_curve
[i
]
2482 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2483 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
2484 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2485 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2486 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2488 # Select the "closing segment", and subdivide it
2489 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_control_point
= True
2490 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_left_handle
= True
2491 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].select_right_handle
= True
2493 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_control_point
= True
2494 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_left_handle
= True
2495 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].select_right_handle
= True
2497 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2499 (ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[0].co
-
2500 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[spline_bp_count
- 1].co
).length
/
2501 self
.average_gp_segment_length
2504 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=segments
)
2506 # Delete the other vertices and make it non-cyclic to
2507 # keep only the needed verts of the "closing segment"
2508 bpy
.ops
.curve
.select_all(action
='INVERT')
2509 bpy
.ops
.curve
.delete(type='VERT')
2510 ob_simplified_curve
[i
].data
.splines
[0].use_cyclic_u
= False
2511 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2513 # Add the points of the "closing segment" to the original curve from grease pencil stroke
2514 first_new_index
= len(self
.main_splines
.data
.splines
[i
].bezier_points
)
2515 self
.main_splines
.data
.splines
[i
].bezier_points
.add(
2516 len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
) - 1
2518 for t
in range(1, len(ob_simplified_curve
[i
].data
.splines
[0].bezier_points
)):
2519 self
.main_splines
.data
.splines
[i
].bezier_points
[t
- 1 + first_new_index
].co
= \
2520 ob_simplified_curve
[i
].data
.splines
[0].bezier_points
[t
].co
2522 # Delete the temporal curve
2523 bpy
.ops
.object.delete({"selected_objects": [ob_simplified_curve
[i
]]})
2525 # Get the coords of the points distributed along the sketched strokes,
2526 # with proportions-U of the first selection
2527 pts_on_strokes_with_proportions_U
= self
.distribute_pts(
2528 self
.main_splines
.data
.splines
,
2531 sketched_splines_parsed
= []
2533 if self
.selection_U2_exists
:
2534 # Initialize the multidimensional list with the proportions of all the segments
2535 proportions_loops_crossing_strokes
= []
2536 for i
in range(len(pts_on_strokes_with_proportions_U
)):
2537 proportions_loops_crossing_strokes
.append([])
2539 for t
in range(len(pts_on_strokes_with_proportions_U
[0])):
2540 proportions_loops_crossing_strokes
[i
].append(None)
2542 # Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
2543 for lp
in range(len(pts_on_strokes_with_proportions_U
[0])):
2544 loop_segments_lengths
= []
2546 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2547 # When on the first stroke, add the segment from the selection to the dirst stroke
2549 loop_segments_lengths
.append(
2550 ((self
.main_object
.matrix_world
@ verts_ordered_U
[lp
].co
) -
2551 pts_on_strokes_with_proportions_U
[0][lp
]).length
2553 # For all strokes except for the last, calculate the distance
2554 # from the actual stroke to the next
2555 if st
!= len(pts_on_strokes_with_proportions_U
) - 1:
2556 loop_segments_lengths
.append(
2557 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2558 pts_on_strokes_with_proportions_U
[st
+ 1][lp
]).length
2560 # When on the last stroke, add the segments
2561 # from the last stroke to the second selection
2562 if st
== len(pts_on_strokes_with_proportions_U
) - 1:
2563 loop_segments_lengths
.append(
2564 (pts_on_strokes_with_proportions_U
[st
][lp
] -
2565 (self
.main_object
.matrix_world
@ verts_ordered_U2
[lp
].co
)).length
2567 # Calculate full loop length
2568 loop_seg_lengths_sum
= 0
2569 for i
in range(len(loop_segments_lengths
)):
2570 loop_seg_lengths_sum
+= loop_segments_lengths
[i
]
2572 # Fill the multidimensional list with the proportions of all the segments
2573 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2574 proportions_loops_crossing_strokes
[st
][lp
] = \
2575 loop_segments_lengths
[st
] / loop_seg_lengths_sum
2577 # Calculate proportions for each stroke
2578 for st
in range(len(pts_on_strokes_with_proportions_U
)):
2579 actual_stroke_spline
= []
2580 # Needs to be a list for the "distribute_pts" method
2581 actual_stroke_spline
.append(self
.main_splines
.data
.splines
[st
])
2583 # Calculate the proportions for the actual stroke.
2584 actual_edges_proportions_U
= []
2585 for i
in range(len(edges_proportions_U
)):
2588 # Sum the proportions of this loop up to the actual.
2589 for t
in range(0, st
+ 1):
2590 proportions_sum
+= proportions_loops_crossing_strokes
[t
][i
]
2591 # i + 1, because proportions_loops_crossing_strokes refers to loops,
2592 # and the proportions refer to edges, so we start at the element 1
2593 # of proportions_loops_crossing_strokes instead of element 0
2594 actual_edges_proportions_U
.append(
2595 edges_proportions_U
[i
] -
2596 ((edges_proportions_U
[i
] - edges_proportions_U2
[i
]) * proportions_sum
)
2598 points_actual_spline
= self
.distribute_pts(actual_stroke_spline
, actual_edges_proportions_U
)
2599 sketched_splines_parsed
.append(points_actual_spline
[0])
2601 sketched_splines_parsed
= pts_on_strokes_with_proportions_U
2603 # If the selection type is "TWO_NOT_CONNECTED" replace the
2604 # points of the last spline with the points in the "target" selection
2605 if selection_type
== "TWO_NOT_CONNECTED":
2606 if self
.selection_U2_exists
:
2607 for i
in range(0, len(sketched_splines_parsed
[len(sketched_splines_parsed
) - 1])):
2608 sketched_splines_parsed
[len(sketched_splines_parsed
) - 1][i
] = \
2609 self
.main_object
.matrix_world
@ verts_ordered_U2
[i
].co
2611 # Create temporary curves along the "control-points" found
2612 # on the sketched curves and the mesh selection
2613 mesh_ctrl_pts_name
= "SURFSKIO_ctrl_pts"
2614 me
= bpy
.data
.meshes
.new(mesh_ctrl_pts_name
)
2615 ob_ctrl_pts
= bpy
.data
.objects
.new(mesh_ctrl_pts_name
, me
)
2616 ob_ctrl_pts
.data
= me
2617 bpy
.context
.collection
.objects
.link(ob_ctrl_pts
)
2624 for i
in range(0, verts_count_U
):
2625 vert_num_in_spline
= 1
2627 if self
.selection_U_exists
:
2628 ob_ctrl_pts
.data
.vertices
.add(1)
2629 last_v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2630 last_v
.co
= self
.main_object
.matrix_world
@ verts_ordered_U
[i
].co
2632 vert_num_in_spline
+= 1
2634 for t
in range(0, len(sketched_splines_parsed
)):
2635 ob_ctrl_pts
.data
.vertices
.add(1)
2636 v
= ob_ctrl_pts
.data
.vertices
[len(ob_ctrl_pts
.data
.vertices
) - 1]
2637 v
.co
= sketched_splines_parsed
[t
][i
]
2639 if vert_num_in_spline
> 1:
2640 ob_ctrl_pts
.data
.edges
.add(1)
2641 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[0] = \
2642 len(ob_ctrl_pts
.data
.vertices
) - 2
2643 ob_ctrl_pts
.data
.edges
[len(ob_ctrl_pts
.data
.edges
) - 1].vertices
[1] = \
2644 len(ob_ctrl_pts
.data
.vertices
) - 1
2647 first_verts
.append(v
.index
)
2650 second_verts
.append(v
.index
)
2652 if t
== len(sketched_splines_parsed
) - 1:
2653 last_verts
.append(v
.index
)
2656 vert_num_in_spline
+= 1
2658 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2659 ob_ctrl_pts
.select_set(True)
2660 bpy
.context
.view_layer
.objects
.active
= ob_ctrl_pts
2662 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2663 bpy
.ops
.mesh
.select_all(action
='DESELECT')
2664 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2666 # Determine which loops-U will be "Cyclic"
2667 for i
in range(0, len(first_verts
)):
2668 # When there is Cyclic Cross there is no need of
2669 # Automatic Join, (and there are at least three strokes)
2670 if self
.automatic_join
and not self
.cyclic_cross
and \
2671 selection_type
!= "TWO_CONNECTED" and len(self
.main_splines
.data
.splines
) >= 3:
2673 v
= ob_ctrl_pts
.data
.vertices
2674 first_point_co
= v
[first_verts
[i
]].co
2675 second_point_co
= v
[second_verts
[i
]].co
2676 last_point_co
= v
[last_verts
[i
]].co
2678 # Coordinates of the point in the center of both the first and last verts.
2680 (first_point_co
[0] + last_point_co
[0]) / 2,
2681 (first_point_co
[1] + last_point_co
[1]) / 2,
2682 (first_point_co
[2] + last_point_co
[2]) / 2
2684 vec_A
= second_point_co
- first_point_co
2685 vec_B
= second_point_co
- Vector(verts_center_co
)
2687 # Calculate the length of the first segment of the loop,
2688 # and the length it would have after moving the first vert
2689 # to the middle position between first and last
2690 length_original
= (second_point_co
- first_point_co
).length
2691 length_target
= (second_point_co
- Vector(verts_center_co
)).length
2693 angle
= vec_A
.angle(vec_B
) / pi
2695 # If the target length doesn't stretch too much, and the
2696 # its angle doesn't change to much either
2697 if length_target
<= length_original
* 1.03 * self
.join_stretch_factor
and \
2698 angle
<= 0.008 * self
.join_stretch_factor
and not self
.selection_U_exists
:
2700 cyclic_loops_U
.append(True)
2701 # Move the first vert to the center coordinates
2702 ob_ctrl_pts
.data
.vertices
[first_verts
[i
]].co
= verts_center_co
2703 # Select the last verts from Cyclic loops, for later deletion all at once
2704 v
[last_verts
[i
]].select_set(True)
2706 cyclic_loops_U
.append(False)
2708 # If "Cyclic Cross" is active then "all" crossing curves become cyclic
2709 if self
.cyclic_cross
and not self
.selection_U_exists
and not \
2710 ((self
.selection_V_exists
and not self
.selection_V_is_closed
) or
2711 (self
.selection_V2_exists
and not self
.selection_V2_is_closed
)):
2713 cyclic_loops_U
.append(True)
2715 cyclic_loops_U
.append(False)
2717 # The cyclic_loops_U list needs to be reversed.
2718 cyclic_loops_U
.reverse()
2720 # Delete the previously selected (last_)verts.
2721 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2722 bpy
.ops
.mesh
.delete('INVOKE_REGION_WIN', type='VERT')
2723 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2725 # Create curves from control points.
2726 bpy
.ops
.object.convert('INVOKE_REGION_WIN', target
='CURVE', keep_original
=False)
2727 ob_curves_surf
= bpy
.context
.view_layer
.objects
.active
2728 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2729 bpy
.ops
.curve
.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
2730 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
2732 # Make Cyclic the splines designated as Cyclic.
2733 for i
in range(0, len(cyclic_loops_U
)):
2734 ob_curves_surf
.data
.splines
[i
].use_cyclic_u
= cyclic_loops_U
[i
]
2736 # Get the coords of all points on first loop-U, for later comparison with its
2737 # subdivided version, to know which points of the loops-U are crossed by the
2738 # original strokes. The indices will be the same for the other loops-U
2739 if self
.loops_on_strokes
:
2740 coords_loops_U_control_points
= []
2741 for p
in ob_ctrl_pts
.data
.splines
[0].bezier_points
:
2742 coords_loops_U_control_points
.append(["%.4f" % p
.co
[0], "%.4f" % p
.co
[1], "%.4f" % p
.co
[2]])
2744 tuple(coords_loops_U_control_points
)
2746 # Calculate number of edges-V in case option "Loops on strokes" is active or inactive
2747 if self
.loops_on_strokes
and not self
.selection_V_exists
:
2748 edges_V_count
= len(self
.main_splines
.data
.splines
) * self
.edges_V
2750 edges_V_count
= len(edges_proportions_V
)
2752 # The Follow precision will vary depending on the number of Follow face-loops
2753 precision_multiplier
= round(2 + (edges_V_count
/ 15))
2754 curve_cuts
= bpy
.context
.scene
.bsurfaces
.SURFSK_precision
* precision_multiplier
2756 # Subdivide the curves
2757 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=curve_cuts
)
2759 # The verts position shifting that happens with splines subdivision.
2760 # For later reorder splines points
2761 verts_position_shift
= curve_cuts
+ 1
2762 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
2764 # Reorder coordinates of the points of each spline to put the first point of
2765 # the spline starting at the position it was the first point before sudividing
2766 # the curve. And make a new curve object per spline (to handle memory better later)
2767 splines_U_objects
= []
2768 for i
in range(len(ob_curves_surf
.data
.splines
)):
2769 spline_U_curve
= bpy
.data
.curves
.new('SURFSKIO_spline_U_' + str(i
), 'CURVE')
2770 ob_spline_U
= bpy
.data
.objects
.new('SURFSKIO_spline_U_' + str(i
), spline_U_curve
)
2771 bpy
.context
.collection
.objects
.link(ob_spline_U
)
2773 spline_U_curve
.dimensions
= "3D"
2775 # Add points to the spline in the new curve object
2776 ob_spline_U
.data
.splines
.new('BEZIER')
2777 for t
in range(len(ob_curves_surf
.data
.splines
[i
].bezier_points
)):
2778 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2779 if t
+ verts_position_shift
<= len(ob_curves_surf
.data
.splines
[i
].bezier_points
) - 1:
2780 point_index
= t
+ verts_position_shift
2782 point_index
= t
+ verts_position_shift
- len(ob_curves_surf
.data
.splines
[i
].bezier_points
)
2785 # to avoid adding the first point since it's added when the spline is created
2787 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2788 ob_spline_U
.data
.splines
[0].bezier_points
[t
].co
= \
2789 ob_curves_surf
.data
.splines
[i
].bezier_points
[point_index
].co
2791 if cyclic_loops_U
[i
] is True and not self
.selection_U_exists
: # If the loop is cyclic
2792 # Add a last point at the same location as the first one
2793 ob_spline_U
.data
.splines
[0].bezier_points
.add(1)
2794 ob_spline_U
.data
.splines
[0].bezier_points
[len(ob_spline_U
.data
.splines
[0].bezier_points
) - 1].co
= \
2795 ob_spline_U
.data
.splines
[0].bezier_points
[0].co
2797 ob_spline_U
.data
.splines
[0].use_cyclic_u
= False
2799 splines_U_objects
.append(ob_spline_U
)
2800 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
2801 ob_spline_U
.select_set(True)
2802 bpy
.context
.view_layer
.objects
.active
= ob_spline_U
2804 # When option "Loops on strokes" is active each "Cross" loop will have
2805 # its own proportions according to where the original strokes "touch" them
2806 if self
.loops_on_strokes
:
2807 # Get the indices of points where the original strokes "touch" loops-U
2808 points_U_crossed_by_strokes
= []
2809 for i
in range(len(splines_U_objects
[0].data
.splines
[0].bezier_points
)):
2810 bp
= splines_U_objects
[0].data
.splines
[0].bezier_points
[i
]
2811 if ["%.4f" % bp
.co
[0], "%.4f" % bp
.co
[1], "%.4f" % bp
.co
[2]] in coords_loops_U_control_points
:
2812 points_U_crossed_by_strokes
.append(i
)
2814 # Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
2815 edge_order_number_for_splines
= {}
2816 if self
.selection_V_exists
:
2817 # For two-connected selections add a first hypothetic stroke at the beginning.
2818 if selection_type
== "TWO_CONNECTED":
2819 edge_order_number_for_splines
[0] = 0
2821 for i
in range(len(self
.main_splines
.data
.splines
)):
2822 sp
= self
.main_splines
.data
.splines
[i
]
2823 v_idx
, dist_temp
= self
.shortest_distance(
2825 sp
.bezier_points
[0].co
,
2826 verts_ordered_V_indices
2828 # Get the position (edges count) of the vert v_idx in the selected chain V
2829 edge_idx_in_chain
= verts_ordered_V_indices
.index(v_idx
)
2831 # For two-connected selections the strokes go after the
2832 # hypothetic stroke added before, so the index adds one per spline
2833 if selection_type
== "TWO_CONNECTED":
2834 spline_number
= i
+ 1
2838 edge_order_number_for_splines
[spline_number
] = edge_idx_in_chain
2840 # Get the first and last verts indices for later comparison
2843 elif i
== len(self
.main_splines
.data
.splines
) - 1:
2846 if self
.selection_V_is_closed
:
2847 # If there is no last stroke on the last vertex (same as first vertex),
2848 # add a hypothetic spline at last vert order
2849 if first_v_idx
!= last_v_idx
:
2850 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2851 len(verts_ordered_V_indices
) - 1
2853 if self
.cyclic_cross
:
2854 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2855 len(verts_ordered_V_indices
) - 2
2856 edge_order_number_for_splines
[(len(self
.main_splines
.data
.splines
) - 1) + 1] = \
2857 len(verts_ordered_V_indices
) - 1
2859 edge_order_number_for_splines
[len(self
.main_splines
.data
.splines
) - 1] = \
2860 len(verts_ordered_V_indices
) - 1
2862 # Get the coords of the points distributed along the
2863 # "crossing curves", with appropriate proportions-V
2864 surface_splines_parsed
= []
2865 for i
in range(len(splines_U_objects
)):
2866 sp_ob
= splines_U_objects
[i
]
2867 # If "Loops on strokes" option is active, calculate the proportions for each loop-U
2868 if self
.loops_on_strokes
:
2869 # Segments distances from stroke to stroke
2872 segments_distances
= []
2873 for t
in range(len(sp_ob
.data
.splines
[0].bezier_points
)):
2874 bp
= sp_ob
.data
.splines
[0].bezier_points
[t
]
2880 dist
+= (last_p
- actual_p
).length
2882 if t
in points_U_crossed_by_strokes
:
2883 segments_distances
.append(dist
)
2890 # Calculate Proportions.
2891 used_edges_proportions_V
= []
2892 for t
in range(len(segments_distances
)):
2893 if self
.selection_V_exists
:
2895 order_number_last_stroke
= 0
2897 segment_edges_length_V
= 0
2898 segment_edges_length_V2
= 0
2899 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2900 segment_edges_length_V
+= edges_lengths_V
[order
]
2901 if self
.selection_V2_exists
:
2902 segment_edges_length_V2
+= edges_lengths_V2
[order
]
2904 for order
in range(order_number_last_stroke
, edge_order_number_for_splines
[t
+ 1]):
2905 # Calculate each "sub-segment" (the ones between each stroke) length
2906 if self
.selection_V2_exists
:
2907 proportion_sub_seg
= (edges_lengths_V2
[order
] -
2908 ((edges_lengths_V2
[order
] - edges_lengths_V
[order
]) /
2909 len(splines_U_objects
) * i
)) / (segment_edges_length_V2
-
2910 (segment_edges_length_V2
- segment_edges_length_V
) /
2911 len(splines_U_objects
) * i
)
2913 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2915 proportion_sub_seg
= edges_lengths_V
[order
] / segment_edges_length_V
2916 sub_seg_dist
= segments_distances
[t
] * proportion_sub_seg
2918 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2920 order_number_last_stroke
= edge_order_number_for_splines
[t
+ 1]
2923 for c
in range(self
.edges_V
):
2924 # Calculate each "sub-segment" (the ones between each stroke) length
2925 sub_seg_dist
= segments_distances
[t
] / self
.edges_V
2926 used_edges_proportions_V
.append(sub_seg_dist
/ full_dist
)
2928 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2929 surface_splines_parsed
.append(actual_spline
[0])
2932 if self
.selection_V2_exists
:
2933 used_edges_proportions_V
= []
2934 for p
in range(len(edges_proportions_V
)):
2935 used_edges_proportions_V
.append(
2936 edges_proportions_V2
[p
] -
2937 ((edges_proportions_V2
[p
] -
2938 edges_proportions_V
[p
]) / len(splines_U_objects
) * i
)
2941 used_edges_proportions_V
= edges_proportions_V
2943 actual_spline
= self
.distribute_pts(sp_ob
.data
.splines
, used_edges_proportions_V
)
2944 surface_splines_parsed
.append(actual_spline
[0])
2946 # Set the verts of the first and last splines to the locations
2947 # of the respective verts in the selections
2948 if self
.selection_V_exists
:
2949 for i
in range(0, len(surface_splines_parsed
[0])):
2950 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = \
2951 self
.main_object
.matrix_world
@ verts_ordered_V
[i
].co
2953 if selection_type
== "TWO_NOT_CONNECTED":
2954 if self
.selection_V2_exists
:
2955 for i
in range(0, len(surface_splines_parsed
[0])):
2956 surface_splines_parsed
[0][i
] = self
.main_object
.matrix_world
@ verts_ordered_V2
[i
].co
2958 # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
2959 # merge the verts of the tips of the loops when they are "near enough"
2960 if self
.automatic_join
and selection_type
!= "TWO_CONNECTED":
2961 # Join the tips of "Follow" loops that are near enough and must be "closed"
2962 if not self
.selection_V_exists
and len(edges_proportions_U
) >= 3:
2963 for i
in range(len(surface_splines_parsed
[0])):
2964 sp
= surface_splines_parsed
2965 loop_segment_dist
= (sp
[0][i
] - sp
[1][i
]).length
2966 full_loop_dist
= loop_segment_dist
* self
.edges_U
2968 verts_middle_position_co
= [
2969 (sp
[0][i
][0] + sp
[len(sp
) - 1][i
][0]) / 2,
2970 (sp
[0][i
][1] + sp
[len(sp
) - 1][i
][1]) / 2,
2971 (sp
[0][i
][2] + sp
[len(sp
) - 1][i
][2]) / 2
2973 points_original
= []
2974 points_original
.append(sp
[1][i
])
2975 points_original
.append(sp
[0][i
])
2978 points_target
.append(sp
[1][i
])
2979 points_target
.append(Vector(verts_middle_position_co
))
2981 vec_A
= points_original
[0] - points_original
[1]
2982 vec_B
= points_target
[0] - points_target
[1]
2983 # check for zero angles, not sure if it is a great fix
2984 if vec_A
.length
!= 0 and vec_B
.length
!= 0:
2985 angle
= vec_A
.angle(vec_B
) / pi
2986 edge_new_length
= (Vector(verts_middle_position_co
) - sp
[1][i
]).length
2991 # If after moving the verts to the middle point, the segment doesn't stretch too much
2992 if edge_new_length
<= loop_segment_dist
* 1.5 * \
2993 self
.join_stretch_factor
and angle
< 0.25 * self
.join_stretch_factor
:
2995 # Avoid joining when the actual loop must be merged with the original mesh
2996 if not (self
.selection_U_exists
and i
== 0) and \
2997 not (self
.selection_U2_exists
and i
== len(surface_splines_parsed
[0]) - 1):
2999 # Change the coords of both verts to the middle position
3000 surface_splines_parsed
[0][i
] = verts_middle_position_co
3001 surface_splines_parsed
[len(surface_splines_parsed
) - 1][i
] = verts_middle_position_co
3003 # Delete object with control points and object from grease pencil conversion
3004 bpy
.ops
.object.delete({"selected_objects": [ob_ctrl_pts
]})
3006 bpy
.ops
.object.delete({"selected_objects": splines_U_objects
})
3010 # Get all verts coords
3011 all_surface_verts_co
= []
3012 for i
in range(0, len(surface_splines_parsed
)):
3013 # Get coords of all verts and make a list with them
3014 for pt_co
in surface_splines_parsed
[i
]:
3015 all_surface_verts_co
.append(pt_co
)
3017 # Define verts for each face
3018 all_surface_faces
= []
3019 for i
in range(0, len(all_surface_verts_co
) - len(surface_splines_parsed
[0])):
3020 if ((i
+ 1) / len(surface_splines_parsed
[0]) != int((i
+ 1) / len(surface_splines_parsed
[0]))):
3021 all_surface_faces
.append(
3022 [i
+ 1, i
, i
+ len(surface_splines_parsed
[0]),
3023 i
+ len(surface_splines_parsed
[0]) + 1]
3026 surf_me_name
= "SURFSKIO_surface"
3027 me_surf
= bpy
.data
.meshes
.new(surf_me_name
)
3029 me_surf
.from_pydata(all_surface_verts_co
, [], all_surface_faces
)
3033 ob_surface
= bpy
.data
.objects
.new(surf_me_name
, me_surf
)
3034 bpy
.context
.collection
.objects
.link(ob_surface
)
3036 # Select all the "unselected but participating" verts, from closed selection
3037 # or double selections with middle-vertex, for later join with remove doubles
3038 for v_idx
in single_unselected_verts
:
3039 self
.main_object
.data
.vertices
[v_idx
].select_set(True)
3041 # Join the new mesh to the main object
3042 ob_surface
.select_set(True)
3043 self
.main_object
.select_set(True)
3044 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3046 bpy
.ops
.object.join('INVOKE_REGION_WIN')
3048 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3050 bpy
.ops
.mesh
.remove_doubles('INVOKE_REGION_WIN', threshold
=0.0001)
3051 bpy
.ops
.mesh
.normals_make_consistent('INVOKE_REGION_WIN', inside
=False)
3052 bpy
.ops
.mesh
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3056 def execute(self
, context
):
3058 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3060 bsurfaces_props
.SURFSK_object_with_strokes
.select_set(True)
3061 self
.main_object
= bsurfaces_props
.SURFSK_object_with_retopology
3062 self
.main_object
.select_set(True)
3063 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3065 bpy
.context
.preferences
.edit
.use_global_undo
= False
3067 if not self
.is_fill_faces
:
3068 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3069 value
='True, False, False')
3071 # Build splines from the "last saved splines".
3072 last_saved_curve
= bpy
.data
.curves
.new('SURFSKIO_last_crv', 'CURVE')
3073 self
.main_splines
= bpy
.data
.objects
.new('SURFSKIO_last_crv', last_saved_curve
)
3074 bpy
.context
.collection
.objects
.link(self
.main_splines
)
3076 last_saved_curve
.dimensions
= "3D"
3078 for sp
in self
.last_strokes_splines_coords
:
3079 spline
= self
.main_splines
.data
.splines
.new('BEZIER')
3080 # less one because one point is added when the spline is created
3081 spline
.bezier_points
.add(len(sp
) - 1)
3082 for p
in range(0, len(sp
)):
3083 spline
.bezier_points
[p
].co
= [sp
[p
][0], sp
[p
][1], sp
[p
][2]]
3085 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3086 bpy
.ops
.object.mode_set(mode
='OBJECT')
3088 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3089 self
.main_splines
.select_set(True)
3090 bpy
.context
.view_layer
.objects
.active
= self
.main_splines
3092 #bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
3093 bpy
.ops
.object.mode_set(mode
='EDIT')
3095 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3096 # Important to make it vector first and then automatic, otherwise the
3097 # tips handles get too big and distort the shrinkwrap results later
3098 bpy
.ops
.curve
.handle_type_set(type='VECTOR')
3099 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
3100 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3101 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3103 self
.main_splines
.name
= "SURFSKIO_temp_strokes"
3105 if self
.is_crosshatch
:
3106 strokes_for_crosshatch
= True
3107 strokes_for_rectangular_surface
= False
3109 strokes_for_rectangular_surface
= True
3110 strokes_for_crosshatch
= False
3112 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3113 self
.main_object
.select_set(True)
3114 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3116 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3118 if strokes_for_rectangular_surface
:
3119 self
.rectangular_surface()
3120 elif strokes_for_crosshatch
:
3121 self
.crosshatch_surface_execute()
3123 # Delete main splines
3124 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3125 bpy
.ops
.object.delete({"selected_objects": [self
.main_splines
]})
3127 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3128 self
.main_object
.select_set(True)
3129 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3131 # Delete grease pencil strokes
3132 if self
.strokes_type
== "GP_STROKES" and not self
.stopping_errors
and not self
.keep_strokes
:
3133 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.data
.layers
[0].clear()
3135 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3137 bpy
.context
.preferences
.edit
.use_global_undo
= self
.initial_global_undo_state
3141 def invoke(self
, context
, event
):
3142 self
.initial_global_undo_state
= bpy
.context
.preferences
.edit
.use_global_undo
3144 bsurfaces_props
= bpy
.context
.scene
.bsurfaces
3145 self
.cyclic_cross
= bsurfaces_props
.SURFSK_cyclic_cross
3146 self
.cyclic_follow
= bsurfaces_props
.SURFSK_cyclic_follow
3147 self
.automatic_join
= bsurfaces_props
.SURFSK_automatic_join
3148 self
.loops_on_strokes
= bsurfaces_props
.SURFSK_loops_on_strokes
3149 self
.keep_strokes
= bsurfaces_props
.SURFSK_keep_strokes
3152 bsurfaces_props
.SURFSK_object_with_strokes
.select_set(True)
3154 self
.report({'WARNING'}, "Specify the name of the object with retopology")
3157 self
.main_object
= bsurfaces_props
.SURFSK_object_with_retopology
3158 self
.main_object
.select_set(True)
3159 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3161 self
.main_object_selected_verts_count
= int(self
.main_object
.data
.total_vert_sel
)
3163 bpy
.context
.preferences
.edit
.use_global_undo
= False
3164 bpy
.ops
.wm
.context_set_value(data_path
='tool_settings.mesh_select_mode',
3165 value
='True, False, False')
3169 if self
.loops_on_strokes
:
3174 self
.is_fill_faces
= False
3175 self
.stopping_errors
= False
3176 self
.last_strokes_splines_coords
= []
3178 # Determine the type of the strokes
3179 self
.strokes_type
= get_strokes_type()
3181 # Check if it will be used grease pencil strokes or curves
3182 # If there are strokes to be used
3183 if self
.strokes_type
== "GP_STROKES" or self
.strokes_type
== "EXTERNAL_CURVE":
3184 if self
.strokes_type
== "GP_STROKES":
3185 # Convert grease pencil strokes to curve
3186 gp
= bsurfaces_props
.SURFSK_object_with_strokes
3187 #bpy.ops.gpencil.convert(type='CURVE', use_link_strokes=False)
3188 self
.original_curve
= conver_gpencil_to_curve(context
, gp
)
3189 # XXX gpencil.convert now keep org object as active/selected, *not* newly created curve!
3190 # XXX This is far from perfect, but should work in most cases...
3191 # self.original_curve = bpy.context.object
3192 gplayer_prefix_translated
= bpy
.app
.translations
.pgettext_data('GP_Layer')
3193 for ob
in bpy
.context
.selected_objects
:
3194 if ob
!= bpy
.context
.view_layer
.objects
.active
and \
3195 ob
.name
.startswith((gplayer_prefix_translated
, 'GP_Layer')):
3196 self
.original_curve
= ob
3197 self
.using_external_curves
= False
3199 elif self
.strokes_type
== "EXTERNAL_CURVE":
3200 for ob
in bpy
.context
.selected_objects
:
3201 if ob
!= bpy
.context
.view_layer
.objects
.active
:
3202 self
.original_curve
= ob
3203 self
.using_external_curves
= True
3205 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3207 # Make sure there are no objects left from erroneous
3208 # executions of this operator, with the reserved names used here
3209 for o
in bpy
.data
.objects
:
3210 if o
.name
.find("SURFSKIO_") != -1:
3211 bpy
.ops
.object.delete({"selected_objects": [o
]})
3213 #bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
3214 self
.original_curve
.select_set(True)
3215 bpy
.context
.view_layer
.objects
.active
= self
.original_curve
3217 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3219 self
.temporary_curve
= bpy
.context
.view_layer
.objects
.active
3221 # Deselect all points of the curve
3222 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3223 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3224 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3226 # Delete splines with only a single isolated point
3227 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3228 sp
= self
.temporary_curve
.data
.splines
[i
]
3230 if len(sp
.bezier_points
) == 1:
3231 sp
.bezier_points
[0].select_control_point
= True
3233 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3234 bpy
.ops
.curve
.delete(type='VERT')
3235 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3237 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3238 self
.temporary_curve
.select_set(True)
3239 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3241 # Set a minimum number of points for crosshatch
3242 minimum_points_num
= 15
3244 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3245 # Check if the number of points of each curve has at least the number of points
3246 # of minimum_points_num, which is a bit more than the face-loops limit.
3247 # If not, subdivide to reach at least that number of points
3248 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3249 sp
= self
.temporary_curve
.data
.splines
[i
]
3251 if len(sp
.bezier_points
) < minimum_points_num
:
3252 for bp
in sp
.bezier_points
:
3253 bp
.select_control_point
= True
3255 if (len(sp
.bezier_points
) - 1) != 0:
3256 # Formula to get the number of cuts that will make a curve
3257 # of N number of points have near to "minimum_points_num"
3258 # points, when subdividing with this number of cuts
3259 subdivide_cuts
= int(
3260 (minimum_points_num
- len(sp
.bezier_points
)) /
3261 (len(sp
.bezier_points
) - 1)
3266 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3267 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3269 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3271 # Detect if the strokes are a crosshatch and do it if it is
3272 self
.crosshatch_surface_invoke(self
.temporary_curve
)
3274 if not self
.is_crosshatch
:
3275 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3276 self
.temporary_curve
.select_set(True)
3277 bpy
.context
.view_layer
.objects
.active
= self
.temporary_curve
3279 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3281 # Set a minimum number of points for rectangular surfaces
3282 minimum_points_num
= 60
3284 # Check if the number of points of each curve has at least the number of points
3285 # of minimum_points_num, which is a bit more than the face-loops limit.
3286 # If not, subdivide to reach at least that number of points
3287 for i
in range(len(self
.temporary_curve
.data
.splines
)):
3288 sp
= self
.temporary_curve
.data
.splines
[i
]
3290 if len(sp
.bezier_points
) < minimum_points_num
:
3291 for bp
in sp
.bezier_points
:
3292 bp
.select_control_point
= True
3294 if (len(sp
.bezier_points
) - 1) != 0:
3295 # Formula to get the number of cuts that will make a curve of
3296 # N number of points have near to "minimum_points_num" points,
3297 # when subdividing with this number of cuts
3298 subdivide_cuts
= int(
3299 (minimum_points_num
- len(sp
.bezier_points
)) /
3300 (len(sp
.bezier_points
) - 1)
3305 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3306 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3308 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3310 # Save coordinates of the actual strokes (as the "last saved splines")
3311 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3312 self
.last_strokes_splines_coords
.append([])
3313 for bp_idx
in range(len(self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
)):
3314 coords
= self
.temporary_curve
.matrix_world
@ \
3315 self
.temporary_curve
.data
.splines
[sp_idx
].bezier_points
[bp_idx
].co
3316 self
.last_strokes_splines_coords
[sp_idx
].append([coords
[0], coords
[1], coords
[2]])
3318 # Check for cyclic splines, put the first and last points in the middle of their actual positions
3319 for sp_idx
in range(len(self
.temporary_curve
.data
.splines
)):
3320 if self
.temporary_curve
.data
.splines
[sp_idx
].use_cyclic_u
is True:
3321 first_p_co
= self
.last_strokes_splines_coords
[sp_idx
][0]
3322 last_p_co
= self
.last_strokes_splines_coords
[sp_idx
][
3323 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3326 (first_p_co
[0] + last_p_co
[0]) / 2,
3327 (first_p_co
[1] + last_p_co
[1]) / 2,
3328 (first_p_co
[2] + last_p_co
[2]) / 2
3331 self
.last_strokes_splines_coords
[sp_idx
][0] = target_co
3332 self
.last_strokes_splines_coords
[sp_idx
][
3333 len(self
.last_strokes_splines_coords
[sp_idx
]) - 1
3335 tuple(self
.last_strokes_splines_coords
)
3337 # Estimation of the average length of the segments between
3338 # each point of the grease pencil strokes.
3339 # Will be useful to determine whether a curve should be made "Cyclic"
3340 segments_lengths_sum
= 0
3342 random_spline
= self
.temporary_curve
.data
.splines
[0].bezier_points
3343 for i
in range(0, len(random_spline
)):
3344 if i
!= 0 and len(random_spline
) - 1 >= i
:
3345 segments_lengths_sum
+= (random_spline
[i
- 1].co
- random_spline
[i
].co
).length
3348 self
.average_gp_segment_length
= segments_lengths_sum
/ segments_count
3350 # Delete temporary strokes curve object
3351 bpy
.ops
.object.delete({"selected_objects": [self
.temporary_curve
]})
3353 #bpy.context.preferences.edit.use_global_undo = False
3355 # If "Keep strokes" option is not active, delete original strokes curve object
3356 if (not self
.stopping_errors
and not self
.keep_strokes
) or self
.is_crosshatch
:
3357 bpy
.ops
.object.delete({"selected_objects": [self
.original_curve
]})
3359 # Delete grease pencil strokes
3360 if self
.strokes_type
== "GP_STROKES" and not self
.stopping_errors
and not self
.keep_strokes
:
3361 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.data
.layers
[0].clear()
3363 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3364 self
.main_object
.select_set(True)
3365 bpy
.context
.view_layer
.objects
.active
= self
.main_object
3367 # Set again since "execute()" will turn it again to its initial value
3368 #bpy.ops.gpencil.surfsk_add_surface()
3369 self
.execute(context
)
3371 bpy
.context
.preferences
.edit
.use_global_undo
= self
.initial_global_undo_state
3373 if not self
.stopping_errors
:
3378 elif self
.strokes_type
== "SELECTION_ALONE":
3379 self
.is_fill_faces
= True
3380 created_faces_count
= self
.fill_with_faces(self
.main_object
)
3382 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3383 bpy
.context
.preferences
.edit
.use_global_undo
= self
.initial_global_undo_state
3385 if created_faces_count
== 0:
3386 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3387 return {"CANCELLED"}
3391 bpy
.context
.preferences
.edit
.use_global_undo
= self
.initial_global_undo_state
3393 if self
.strokes_type
== "EXTERNAL_NO_CURVE":
3394 self
.report({'WARNING'}, "The secondary object is not a Curve.")
3397 elif self
.strokes_type
== "MORE_THAN_ONE_EXTERNAL":
3398 self
.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3401 elif self
.strokes_type
== "SINGLE_GP_STROKE_NO_SELECTION" or \
3402 self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3404 self
.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
3407 elif self
.strokes_type
== "NO_STROKES":
3408 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3411 elif self
.strokes_type
== "CURVE_WITH_NON_BEZIER_SPLINES":
3412 self
.report({'WARNING'}, "All splines must be Bezier.")
3418 # Edit strokes operator
3419 class GPENCIL_OT_SURFSK_init(Operator
):
3420 bl_idname
= "gpencil.surfsk_init"
3421 bl_label
= "Bsurfaces initialize"
3422 bl_description
= "Bsurfaces initialiaze"
3424 def execute(self
, context
):
3425 #bpy.ops.object.mode_set(mode='OBJECT')
3426 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3427 mesh
= bpy
.data
.meshes
.new('BSurfaceMesh')
3428 mesh_object
= object_utils
.object_data_add(context
, mesh
, operator
=None)
3429 mesh_object
.select_set(True)
3430 bpy
.context
.view_layer
.objects
.active
= mesh_object
3431 bpy
.ops
.object.modifier_add(type='SHRINKWRAP')
3433 bpy
.ops
.object.gpencil_add(radius
=1.0, view_align
=False, location
=(0.0, 0.0, 0.0), rotation
=(0.0, 0.0, 0.0), type='EMPTY')
3434 gpencil_object
= bpy
.context
.scene
.objects
[bpy
.context
.scene
.objects
[-1].name
]
3435 gpencil_object
.select_set(True)
3436 bpy
.context
.view_layer
.objects
.active
= gpencil_object
3437 bpy
.ops
.object.mode_set(mode
='PAINT_GPENCIL')
3439 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_retopology
= mesh_object
3440 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
= gpencil_object
3444 # Edit strokes operator
3445 class GPENCIL_OT_SURFSK_add_strokes(Operator
):
3446 bl_idname
= "gpencil.surfsk_add_strokes"
3447 bl_label
= "Bsurfaces add strokes"
3448 bl_description
= "Add the grease pencil strokes"
3450 def execute(self
, context
):
3451 # Determine the type of the strokes
3452 self
.strokes_type
= get_strokes_type()
3453 # Check if strokes are grease pencil strokes or a curves object
3454 selected_objs
= bpy
.context
.selected_objects
3455 if self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3456 for ob
in selected_objs
:
3457 if ob
!= bpy
.context
.view_layer
.objects
.active
:
3460 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3462 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3463 curve_ob
.select_set(True)
3464 bpy
.context
.view_layer
.objects
.active
= curve_ob
3466 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3468 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.select_set(True)
3469 bpy
.context
.view_layer
.objects
.active
= bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
3470 bpy
.ops
.object.mode_set(mode
='PAINT_GPENCIL')
3474 def invoke(self
, context
, event
):
3476 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.select_set(True)
3478 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3481 self
.execute(context
)
3486 # Edit strokes operator
3487 class GPENCIL_OT_SURFSK_edit_strokes(Operator
):
3488 bl_idname
= "gpencil.surfsk_edit_strokes"
3489 bl_label
= "Bsurfaces edit strokes"
3490 bl_description
= "Edit the grease pencil strokes or curves used"
3492 def execute(self
, context
):
3493 # Determine the type of the strokes
3494 self
.strokes_type
= get_strokes_type()
3495 # Check if strokes are grease pencil strokes or a curves object
3496 selected_objs
= bpy
.context
.selected_objects
3497 if self
.strokes_type
== "EXTERNAL_CURVE" or self
.strokes_type
== "SINGLE_CURVE_STROKE_NO_SELECTION":
3498 for ob
in selected_objs
:
3499 if ob
!= bpy
.context
.view_layer
.objects
.active
:
3502 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3504 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3505 curve_ob
.select_set(True)
3506 bpy
.context
.view_layer
.objects
.active
= curve_ob
3508 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3509 elif self
.strokes_type
== "GP_STROKES" or self
.strokes_type
== "SINGLE_GP_STROKE_NO_SELECTION":
3510 # Convert grease pencil strokes to curve
3511 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3512 #bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
3513 gp
= bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
3514 conver_gpencil_to_curve(context
, gp
)
3515 for ob
in bpy
.context
.selected_objects
:
3516 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.name
.startswith("GP_Layer"):
3519 ob_gp_strokes
= bpy
.context
.object
3521 # Delete grease pencil strokes
3522 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.data
.layers
[0].clear()
3525 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3526 ob_gp_strokes
.select_set(True)
3527 bpy
.context
.view_layer
.objects
.active
= ob_gp_strokes
3529 curve_crv
= ob_gp_strokes
.data
3530 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3531 bpy
.ops
.curve
.spline_type_set('INVOKE_REGION_WIN', type="BEZIER")
3532 bpy
.ops
.curve
.handle_type_set('INVOKE_REGION_WIN', type="AUTOMATIC")
3533 #curve_crv.show_handles = False
3534 #curve_crv.show_normal_face = False
3536 elif self
.strokes_type
== "EXTERNAL_NO_CURVE":
3537 self
.report({'WARNING'}, "The secondary object is not a Curve.")
3540 elif self
.strokes_type
== "MORE_THAN_ONE_EXTERNAL":
3541 self
.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
3544 elif self
.strokes_type
== "NO_STROKES" or self
.strokes_type
== "SELECTION_ALONE":
3545 self
.report({'WARNING'}, "There aren't any strokes attached to the object")
3551 def invoke(self
, context
, event
):
3553 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.select_set(True)
3555 self
.report({'WARNING'}, "Specify the name of the object with strokes")
3558 self
.execute(context
)
3563 class CURVE_OT_SURFSK_reorder_splines(Operator
):
3564 bl_idname
= "curve.surfsk_reorder_splines"
3565 bl_label
= "Bsurfaces reorder splines"
3566 bl_description
= "Defines the order of the splines by using grease pencil strokes"
3567 bl_options
= {'REGISTER', 'UNDO'}
3569 def execute(self
, context
):
3570 objects_to_delete
= []
3571 # Convert grease pencil strokes to curve.
3572 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3573 bpy
.ops
.gpencil
.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes
=False)
3574 for ob
in bpy
.context
.selected_objects
:
3575 if ob
!= bpy
.context
.view_layer
.objects
.active
and ob
.name
.startswith("GP_Layer"):
3576 GP_strokes_curve
= ob
3578 # GP_strokes_curve = bpy.context.object
3579 objects_to_delete
.append(GP_strokes_curve
)
3581 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3582 GP_strokes_curve
.select_set(True)
3583 bpy
.context
.view_layer
.objects
.active
= GP_strokes_curve
3585 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3586 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='SELECT')
3587 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=100)
3588 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3590 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3591 GP_strokes_mesh
= bpy
.context
.object
3592 objects_to_delete
.append(GP_strokes_mesh
)
3594 GP_strokes_mesh
.data
.resolution_u
= 1
3595 bpy
.ops
.object.convert(target
='MESH', keep_original
=False)
3597 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3598 self
.main_curve
.select_set(True)
3599 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
3601 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3602 curves_duplicate_1
= bpy
.context
.object
3603 objects_to_delete
.append(curves_duplicate_1
)
3605 minimum_points_num
= 500
3607 # Some iterations since the subdivision operator
3608 # has a limit of 100 subdivisions per iteration
3609 for x
in range(round(minimum_points_num
/ 100)):
3610 # Check if the number of points of each curve has at least the number of points
3611 # of minimum_points_num. If not, subdivide to reach at least that number of points
3612 for i
in range(len(curves_duplicate_1
.data
.splines
)):
3613 sp
= curves_duplicate_1
.data
.splines
[i
]
3615 if len(sp
.bezier_points
) < minimum_points_num
:
3616 for bp
in sp
.bezier_points
:
3617 bp
.select_control_point
= True
3619 if (len(sp
.bezier_points
) - 1) != 0:
3620 # Formula to get the number of cuts that will make a curve of N
3621 # number of points have near to "minimum_points_num" points,
3622 # when subdividing with this number of cuts
3623 subdivide_cuts
= int(
3624 (minimum_points_num
- len(sp
.bezier_points
)) /
3625 (len(sp
.bezier_points
) - 1)
3630 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3631 bpy
.ops
.curve
.subdivide('INVOKE_REGION_WIN', number_cuts
=subdivide_cuts
)
3632 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3633 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3635 bpy
.ops
.object.duplicate('INVOKE_REGION_WIN')
3636 curves_duplicate_2
= bpy
.context
.object
3637 objects_to_delete
.append(curves_duplicate_2
)
3639 # Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
3640 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3641 curves_duplicate_2
.select_set(True)
3642 bpy
.context
.view_layer
.objects
.active
= curves_duplicate_2
3644 bpy
.ops
.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
3645 curves_duplicate_2
.modifiers
["Shrinkwrap"].wrap_method
= "NEAREST_VERTEX"
3646 curves_duplicate_2
.modifiers
["Shrinkwrap"].target
= GP_strokes_mesh
3647 bpy
.ops
.object.modifier_apply('INVOKE_REGION_WIN', apply_as
='DATA', modifier
='Shrinkwrap')
3649 # Get the distance of each vert from its original position to its position with Shrinkwrap
3650 nearest_points_coords
= {}
3651 for st_idx
in range(len(curves_duplicate_1
.data
.splines
)):
3652 for bp_idx
in range(len(curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
)):
3653 bp_1_co
= curves_duplicate_1
.matrix_world
@ \
3654 curves_duplicate_1
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3656 bp_2_co
= curves_duplicate_2
.matrix_world
@ \
3657 curves_duplicate_2
.data
.splines
[st_idx
].bezier_points
[bp_idx
].co
3660 shortest_dist
= (bp_1_co
- bp_2_co
).length
3661 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
3662 "%.4f" % bp_2_co
[1],
3663 "%.4f" % bp_2_co
[2])
3665 dist
= (bp_1_co
- bp_2_co
).length
3667 if dist
< shortest_dist
:
3668 nearest_points_coords
[st_idx
] = ("%.4f" % bp_2_co
[0],
3669 "%.4f" % bp_2_co
[1],
3670 "%.4f" % bp_2_co
[2])
3671 shortest_dist
= dist
3673 # Get all coords of GP strokes points, for comparison
3674 GP_strokes_coords
= []
3675 for st_idx
in range(len(GP_strokes_curve
.data
.splines
)):
3676 GP_strokes_coords
.append(
3677 [("%.4f" % x
if "%.4f" % x
!= "-0.00" else "0.00",
3678 "%.4f" % y
if "%.4f" % y
!= "-0.00" else "0.00",
3679 "%.4f" % z
if "%.4f" % z
!= "-0.00" else "0.00") for
3680 x
, y
, z
in [bp
.co
for bp
in GP_strokes_curve
.data
.splines
[st_idx
].bezier_points
]]
3683 # Check the point of the GP strokes with the same coords as
3684 # the nearest points of the curves (with shrinkwrap)
3686 # Dictionary with GP stroke index as index, and a list as value.
3687 # The list has as index the point index of the GP stroke
3688 # nearest to the spline, and as value the spline index
3689 GP_connection_points
= {}
3690 for gp_st_idx
in range(len(GP_strokes_coords
)):
3691 GPvert_spline_relationship
= {}
3693 for splines_st_idx
in range(len(nearest_points_coords
)):
3694 if nearest_points_coords
[splines_st_idx
] in GP_strokes_coords
[gp_st_idx
]:
3695 GPvert_spline_relationship
[
3696 GP_strokes_coords
[gp_st_idx
].index(nearest_points_coords
[splines_st_idx
])
3699 GP_connection_points
[gp_st_idx
] = GPvert_spline_relationship
3701 # Get the splines new order
3702 splines_new_order
= []
3703 for i
in GP_connection_points
:
3704 dict_keys
= sorted(GP_connection_points
[i
].keys()) # Sort dictionaries by key
3707 splines_new_order
.append(GP_connection_points
[i
][k
])
3710 curve_original_name
= self
.main_curve
.name
3712 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3713 self
.main_curve
.select_set(True)
3714 bpy
.context
.view_layer
.objects
.active
= self
.main_curve
3716 self
.main_curve
.name
= "SURFSKIO_CRV_ORD"
3718 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3719 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3720 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3722 for sp_idx
in range(len(self
.main_curve
.data
.splines
)):
3723 self
.main_curve
.data
.splines
[0].bezier_points
[0].select_control_point
= True
3725 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3726 bpy
.ops
.curve
.separate('EXEC_REGION_WIN')
3727 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3729 # Get the names of the separated splines objects in the original order
3730 splines_unordered
= {}
3731 for o
in bpy
.data
.objects
:
3732 if o
.name
.find("SURFSKIO_CRV_ORD") != -1:
3733 spline_order_string
= o
.name
.partition(".")[2]
3735 if spline_order_string
!= "" and int(spline_order_string
) > 0:
3736 spline_order_index
= int(spline_order_string
) - 1
3737 splines_unordered
[spline_order_index
] = o
.name
3739 # Join all splines objects in final order
3740 for order_idx
in splines_new_order
:
3741 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3742 bpy
.data
.objects
[splines_unordered
[order_idx
]].select_set(True)
3743 bpy
.data
.objects
["SURFSKIO_CRV_ORD"].select_set(True)
3744 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
["SURFSKIO_CRV_ORD"]
3746 bpy
.ops
.object.join('INVOKE_REGION_WIN')
3748 # Go back to the original name of the curves object.
3749 bpy
.context
.object.name
= curve_original_name
3751 # Delete all unused objects
3752 bpy
.ops
.object.delete({"selected_objects": objects_to_delete
})
3754 bpy
.ops
.object.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3755 bpy
.data
.objects
[curve_original_name
].select_set(True)
3756 bpy
.context
.view_layer
.objects
.active
= bpy
.data
.objects
[curve_original_name
]
3758 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3759 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3761 bpy
.context
.scene
.bsurfaces
.SURFSK_object_with_strokes
.data
.layers
[0].clear()
3765 def invoke(self
, context
, event
):
3766 self
.main_curve
= bpy
.context
.object
3767 there_are_GP_strokes
= False
3770 # Get the active grease pencil layer
3771 strokes_num
= len(self
.main_curve
.grease_pencil
.layers
.active
.active_frame
.strokes
)
3774 there_are_GP_strokes
= True
3778 if there_are_GP_strokes
:
3779 self
.execute(context
)
3780 self
.report({'INFO'}, "Splines have been reordered")
3782 self
.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
3787 class CURVE_OT_SURFSK_first_points(Operator
):
3788 bl_idname
= "curve.surfsk_first_points"
3789 bl_label
= "Bsurfaces set first points"
3790 bl_description
= "Set the selected points as the first point of each spline"
3791 bl_options
= {'REGISTER', 'UNDO'}
3793 def execute(self
, context
):
3794 splines_to_invert
= []
3796 # Check non-cyclic splines to invert
3797 for i
in range(len(self
.main_curve
.data
.splines
)):
3798 b_points
= self
.main_curve
.data
.splines
[i
].bezier_points
3800 if i
not in self
.cyclic_splines
: # Only for non-cyclic splines
3801 if b_points
[len(b_points
) - 1].select_control_point
:
3802 splines_to_invert
.append(i
)
3804 # Reorder points of cyclic splines, and set all handles to "Automatic"
3806 # Check first selected point
3807 cyclic_splines_new_first_pt
= {}
3808 for i
in self
.cyclic_splines
:
3809 sp
= self
.main_curve
.data
.splines
[i
]
3811 for t
in range(len(sp
.bezier_points
)):
3812 bp
= sp
.bezier_points
[t
]
3813 if bp
.select_control_point
or bp
.select_right_handle
or bp
.select_left_handle
:
3814 cyclic_splines_new_first_pt
[i
] = t
3815 break # To take only one if there are more
3818 for spline_idx
in cyclic_splines_new_first_pt
:
3819 sp
= self
.main_curve
.data
.splines
[spline_idx
]
3821 spline_old_coords
= []
3822 for bp_old
in sp
.bezier_points
:
3823 coords
= (bp_old
.co
[0], bp_old
.co
[1], bp_old
.co
[2])
3825 left_handle_type
= str(bp_old
.handle_left_type
)
3826 left_handle_length
= float(bp_old
.handle_left
.length
)
3828 float(bp_old
.handle_left
.x
),
3829 float(bp_old
.handle_left
.y
),
3830 float(bp_old
.handle_left
.z
)
3832 right_handle_type
= str(bp_old
.handle_right_type
)
3833 right_handle_length
= float(bp_old
.handle_right
.length
)
3834 right_handle_xyz
= (
3835 float(bp_old
.handle_right
.x
),
3836 float(bp_old
.handle_right
.y
),
3837 float(bp_old
.handle_right
.z
)
3839 spline_old_coords
.append(
3840 [coords
, left_handle_type
,
3841 right_handle_type
, left_handle_length
,
3842 right_handle_length
, left_handle_xyz
,
3846 for t
in range(len(sp
.bezier_points
)):
3847 bp
= sp
.bezier_points
3849 if t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 <= len(bp
) - 1:
3850 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1
3852 new_index
= t
+ cyclic_splines_new_first_pt
[spline_idx
] + 1 - len(bp
)
3854 bp
[t
].co
= Vector(spline_old_coords
[new_index
][0])
3856 bp
[t
].handle_left
.length
= spline_old_coords
[new_index
][3]
3857 bp
[t
].handle_right
.length
= spline_old_coords
[new_index
][4]
3859 bp
[t
].handle_left_type
= "FREE"
3860 bp
[t
].handle_right_type
= "FREE"
3862 bp
[t
].handle_left
.x
= spline_old_coords
[new_index
][5][0]
3863 bp
[t
].handle_left
.y
= spline_old_coords
[new_index
][5][1]
3864 bp
[t
].handle_left
.z
= spline_old_coords
[new_index
][5][2]
3866 bp
[t
].handle_right
.x
= spline_old_coords
[new_index
][6][0]
3867 bp
[t
].handle_right
.y
= spline_old_coords
[new_index
][6][1]
3868 bp
[t
].handle_right
.z
= spline_old_coords
[new_index
][6][2]
3870 bp
[t
].handle_left_type
= spline_old_coords
[new_index
][1]
3871 bp
[t
].handle_right_type
= spline_old_coords
[new_index
][2]
3873 # Invert the non-cyclic splines designated above
3874 for i
in range(len(splines_to_invert
)):
3875 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3877 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3878 self
.main_curve
.data
.splines
[splines_to_invert
[i
]].bezier_points
[0].select_control_point
= True
3879 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3881 bpy
.ops
.curve
.switch_direction()
3883 bpy
.ops
.curve
.select_all('INVOKE_REGION_WIN', action
='DESELECT')
3885 # Keep selected the first vert of each spline
3886 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3887 for i
in range(len(self
.main_curve
.data
.splines
)):
3888 if not self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
3889 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[0]
3891 bp
= self
.main_curve
.data
.splines
[i
].bezier_points
[
3892 len(self
.main_curve
.data
.splines
[i
].bezier_points
) - 1
3895 bp
.select_control_point
= True
3896 bp
.select_right_handle
= True
3897 bp
.select_left_handle
= True
3899 bpy
.ops
.object.editmode_toggle('INVOKE_REGION_WIN')
3903 def invoke(self
, context
, event
):
3904 self
.main_curve
= bpy
.context
.object
3906 # Check if all curves are Bezier, and detect which ones are cyclic
3907 self
.cyclic_splines
= []
3908 for i
in range(len(self
.main_curve
.data
.splines
)):
3909 if self
.main_curve
.data
.splines
[i
].type != "BEZIER":
3910 self
.report({'WARNING'}, "All splines must be Bezier type")
3912 return {'CANCELLED'}
3914 if self
.main_curve
.data
.splines
[i
].use_cyclic_u
:
3915 self
.cyclic_splines
.append(i
)
3917 self
.execute(context
)
3918 self
.report({'INFO'}, "First points have been set")
3923 # Add-ons Preferences Update Panel
3925 # Define Panel classes for updating
3927 VIEW3D_PT_tools_SURFSK_mesh
,
3928 VIEW3D_PT_tools_SURFSK_curve
3932 def update_panel(self
, context
):
3933 message
= "Bsurfaces GPL Edition: Updating Panel locations has failed"
3935 for panel
in panels
:
3936 if "bl_rna" in panel
.__dict
__:
3937 bpy
.utils
.unregister_class(panel
)
3939 for panel
in panels
:
3940 panel
.bl_category
= context
.preferences
.addons
[__name__
].preferences
.category
3941 bpy
.utils
.register_class(panel
)
3943 except Exception as e
:
3944 print("\n[{}]\n{}\n\nError:\n{}".format(__name__
, message
, e
))
3947 def conver_gpencil_to_curve(context
, pencil
):
3948 newCurve
= bpy
.data
.curves
.new('gpencil_curve', type='CURVE') # curvedatablock
3949 newCurve
.dimensions
= '3D'
3950 CurveObject
= object_utils
.object_data_add(context
, newCurve
) # place in active scene
3952 for i
, stroke
in enumerate(pencil
.data
.layers
[0].active_frame
.strokes
):
3953 stroke_points
= pencil
.data
.layers
[0].active_frame
.strokes
[i
].points
3954 data_list
= [ (point
.co
.x
, point
.co
.y
, point
.co
.z
)
3955 for point
in stroke_points
]
3956 points_to_add
= len(data_list
)-1
3959 for point
in data_list
:
3960 flat_list
.extend(point
)
3962 spline
= newCurve
.splines
.new(type='BEZIER') # spline
3963 spline
.bezier_points
.add(points_to_add
)
3964 spline
.bezier_points
.foreach_set("co", flat_list
)
3966 for point
in spline
.bezier_points
:
3967 point
.handle_left_type
="AUTO"
3968 point
.handle_right_type
="AUTO"
3972 class BsurfPreferences(AddonPreferences
):
3973 # this must match the addon name, use '__package__'
3974 # when defining this in a submodule of a python package.
3975 bl_idname
= __name__
3977 category
: StringProperty(
3978 name
="Tab Category",
3979 description
="Choose a name for the category of the panel",
3984 def draw(self
, context
):
3985 layout
= self
.layout
3989 col
.label(text
="Tab Category:")
3990 col
.prop(self
, "category", text
="")
3994 class BsurfacesProps(PropertyGroup
):
3995 SURFSK_cyclic_cross
: BoolProperty(
3996 name
="Cyclic Cross",
3997 description
="Make cyclic the face-loops crossing the strokes",
4000 SURFSK_cyclic_follow
: BoolProperty(
4001 name
="Cyclic Follow",
4002 description
="Make cyclic the face-loops following the strokes",
4005 SURFSK_keep_strokes
: BoolProperty(
4006 name
="Keep strokes",
4007 description
="Keeps the sketched strokes or curves after adding the surface",
4010 SURFSK_automatic_join
: BoolProperty(
4011 name
="Automatic join",
4012 description
="Join automatically vertices of either surfaces "
4013 "generated by crosshatching, or from the borders of closed shapes",
4016 SURFSK_loops_on_strokes
: BoolProperty(
4017 name
="Loops on strokes",
4018 description
="Make the loops match the paths of the strokes",
4021 SURFSK_precision
: IntProperty(
4023 description
="Precision level of the surface calculation",
4028 SURFSK_object_with_retopology
: PointerProperty(
4030 type=bpy
.types
.Object
4032 SURFSK_object_with_strokes
: PointerProperty(
4034 type=bpy
.types
.Object
4038 GPENCIL_OT_SURFSK_add_surface
,
4039 GPENCIL_OT_SURFSK_add_strokes
,
4040 GPENCIL_OT_SURFSK_edit_strokes
,
4041 CURVE_OT_SURFSK_reorder_splines
,
4042 CURVE_OT_SURFSK_first_points
,
4045 GPENCIL_OT_SURFSK_init
4050 bpy
.utils
.register_class(cls
)
4052 bpy
.types
.Scene
.bsurfaces
= PointerProperty(type=BsurfacesProps
)
4053 update_panel(None, bpy
.context
)
4057 bpy
.utils
.unregister_class(cls
)
4059 del bpy
.types
.Scene
.bsurfaces
4061 if __name__
== "__main__":