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